@@ -326,6 +326,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
onComponentChange={this.handleComponentChange} | |||
warnings={this.state.warnings} | |||
/> | |||
)} |
@@ -29,6 +29,8 @@ import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; | |||
import Header from './Header'; | |||
import HeaderMeta from './HeaderMeta'; | |||
import Menu from './Menu'; | |||
import InfoDrawer from './projectInformation/InfoDrawer'; | |||
import ProjectInformation from './projectInformation/ProjectInformation'; | |||
interface Props { | |||
branchLikes: BranchLike[]; | |||
@@ -38,24 +40,27 @@ interface Props { | |||
currentTaskOnSameBranch?: boolean; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onComponentChange: (changes: Partial<T.Component>) => void; | |||
warnings: string[]; | |||
} | |||
export default class ComponentNav extends React.PureComponent<Props> { | |||
mounted = false; | |||
export default function ComponentNav(props: Props) { | |||
const { | |||
branchLikes, | |||
component, | |||
currentBranchLike, | |||
currentTask, | |||
currentTaskOnSameBranch, | |||
isInProgress, | |||
isPending, | |||
warnings | |||
} = props; | |||
const { contextNavHeightRaw, globalNavHeightRaw } = rawSizes; | |||
componentDidMount() { | |||
this.populateRecentHistory(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.component.key !== prevProps.component.key) { | |||
this.populateRecentHistory(); | |||
} | |||
} | |||
const [displayProjectInfo, setDisplayProjectInfo] = React.useState(false); | |||
populateRecentHistory = () => { | |||
const { breadcrumbs } = this.props.component; | |||
React.useEffect(() => { | |||
const { breadcrumbs, key, name, organization } = component; | |||
const { qualifier } = breadcrumbs[breadcrumbs.length - 1]; | |||
if ( | |||
[ | |||
@@ -65,55 +70,53 @@ export default class ComponentNav extends React.PureComponent<Props> { | |||
ComponentQualifier.Developper | |||
].includes(qualifier as ComponentQualifier) | |||
) { | |||
RecentHistory.add( | |||
this.props.component.key, | |||
this.props.component.name, | |||
qualifier.toLowerCase(), | |||
this.props.component.organization | |||
); | |||
RecentHistory.add(key, name, qualifier.toLowerCase(), organization); | |||
} | |||
}; | |||
}, [component, component.key]); | |||
render() { | |||
const { component, currentBranchLike, currentTask, isInProgress, isPending } = this.props; | |||
const contextNavHeight = rawSizes.contextNavHeightRaw; | |||
let notifComponent; | |||
if (isInProgress || isPending || (currentTask && currentTask.status === STATUSES.FAILED)) { | |||
notifComponent = ( | |||
<ComponentNavBgTaskNotif | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={this.props.currentTaskOnSameBranch} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
/> | |||
); | |||
} | |||
return ( | |||
<ContextNavBar | |||
height={notifComponent ? contextNavHeight + 30 : contextNavHeight} | |||
id="context-navigation" | |||
notif={notifComponent}> | |||
<div | |||
className={classNames( | |||
'display-flex-center display-flex-space-between little-padded-top', | |||
{ | |||
'padded-bottom': this.props.warnings.length === 0 | |||
} | |||
)}> | |||
<Header | |||
branchLikes={this.props.branchLikes} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
/> | |||
<HeaderMeta | |||
branchLike={currentBranchLike} | |||
component={component} | |||
warnings={this.props.warnings} | |||
/> | |||
</div> | |||
<Menu branchLike={currentBranchLike} component={component} /> | |||
</ContextNavBar> | |||
let notifComponent; | |||
if (isInProgress || isPending || (currentTask && currentTask.status === STATUSES.FAILED)) { | |||
notifComponent = ( | |||
<ComponentNavBgTaskNotif | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTaskOnSameBranch} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
/> | |||
); | |||
} | |||
const contextNavHeight = notifComponent ? contextNavHeightRaw + 30 : contextNavHeightRaw; | |||
return ( | |||
<ContextNavBar height={contextNavHeight} id="context-navigation" notif={notifComponent}> | |||
<div | |||
className={classNames('display-flex-center display-flex-space-between little-padded-top', { | |||
'padded-bottom': warnings.length === 0 | |||
})}> | |||
<Header | |||
branchLikes={branchLikes} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
/> | |||
<HeaderMeta branchLike={currentBranchLike} component={component} warnings={warnings} /> | |||
</div> | |||
<Menu | |||
branchLike={currentBranchLike} | |||
component={component} | |||
onToggleProjectInfo={() => setDisplayProjectInfo(!displayProjectInfo)} | |||
/> | |||
<InfoDrawer | |||
displayed={displayProjectInfo} | |||
onClose={() => setDisplayProjectInfo(false)} | |||
top={globalNavHeightRaw + contextNavHeightRaw}> | |||
<ProjectInformation | |||
branchLike={currentBranchLike} | |||
component={component} | |||
onComponentChange={props.onComponentChange} | |||
/> | |||
</InfoDrawer> | |||
</ContextNavBar> | |||
); | |||
} |
@@ -0,0 +1,27 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
.navbar-tabs > li > a.menu-button { | |||
color: var(--darkBlue); | |||
} | |||
.navbar-tabs > li > a.menu-button:hover { | |||
color: var(--blue); | |||
border-bottom-color: transparent; | |||
} |
@@ -21,6 +21,7 @@ import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; | |||
import BulletListIcon from 'sonar-ui-common/components/icons/BulletListIcon'; | |||
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; | |||
import NavBarTabs from 'sonar-ui-common/components/ui/NavBarTabs'; | |||
import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n'; | |||
@@ -29,6 +30,7 @@ import { getBranchLikeQuery, isMainBranch, isPullRequest } from '../../../../hel | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import './Menu.css'; | |||
const SETTINGS_URLS = [ | |||
'/project/admin', | |||
@@ -51,6 +53,7 @@ interface Props { | |||
appState: Pick<T.AppState, 'branchesEnabled'>; | |||
branchLike: BranchLike | undefined; | |||
component: T.Component; | |||
onToggleProjectInfo: () => void; | |||
} | |||
export class Menu extends React.PureComponent<Props> { | |||
@@ -249,6 +252,31 @@ export class Menu extends React.PureComponent<Props> { | |||
]; | |||
} | |||
renderProjectInformationButton() { | |||
if (isPullRequest(this.props.branchLike)) { | |||
return null; | |||
} | |||
return ( | |||
(this.isProject() || this.isApplication()) && ( | |||
<li> | |||
<a | |||
className="menu-button" | |||
onClick={(e: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
this.props.onToggleProjectInfo(); | |||
}} | |||
role="button" | |||
tabIndex={0}> | |||
<BulletListIcon className="little-spacer-right" /> | |||
{translate(this.isProject() ? 'project' : 'application', 'info.title')} | |||
</a> | |||
</li> | |||
) | |||
); | |||
} | |||
renderSettingsLink() { | |||
if (!this.getConfiguration().showSettings || this.isApplication() || this.isPortfolio()) { | |||
return null; | |||
@@ -511,7 +539,10 @@ export class Menu extends React.PureComponent<Props> { | |||
{this.renderActivityLink()} | |||
{this.renderExtensions()} | |||
</NavBarTabs> | |||
<NavBarTabs>{this.renderAdministration()}</NavBarTabs> | |||
<NavBarTabs> | |||
{this.renderAdministration()} | |||
{this.renderProjectInformationButton()} | |||
</NavBarTabs> | |||
</div> | |||
); | |||
} |
@@ -35,9 +35,11 @@ it('renders', () => { | |||
branchLikes={[]} | |||
component={component} | |||
currentBranchLike={undefined} | |||
isInProgress={true} | |||
isPending={true} | |||
onComponentChange={jest.fn()} | |||
warnings={[]} | |||
/> | |||
); | |||
wrapper.setState({ isInProgress: true, isPending: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -44,9 +44,7 @@ it('should work with extensions', () => { | |||
configuration: { showSettings: true, extensions: [{ key: 'foo', name: 'Foo' }] }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
}; | |||
const wrapper = shallow( | |||
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} /> | |||
); | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); | |||
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); | |||
}); | |||
@@ -66,9 +64,7 @@ it('should work with multiple extensions', () => { | |||
{ key: 'component-bar', name: 'ComponentBar' } | |||
] | |||
}; | |||
const wrapper = shallow( | |||
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} /> | |||
); | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); | |||
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); | |||
}); | |||
@@ -88,30 +84,25 @@ it('should render correctly for security extensions', () => { | |||
{ key: 'component-bar', name: 'ComponentBar' } | |||
] | |||
}; | |||
const wrapper = shallow( | |||
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} /> | |||
); | |||
const wrapper = shallowRender({ component }); | |||
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); | |||
expect(wrapper.find('Dropdown[data-test="security"]')).toMatchSnapshot(); | |||
}); | |||
it('should work for a branch', () => { | |||
const branch = mockBranch({ | |||
const branchLike = mockBranch({ | |||
name: 'release' | |||
}); | |||
[true, false].forEach(showSettings => | |||
expect( | |||
shallow( | |||
<Menu | |||
appState={{ branchesEnabled: true }} | |||
branchLike={branch} | |||
component={{ | |||
...baseComponent, | |||
configuration: { showSettings }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
}} | |||
/> | |||
) | |||
shallowRender({ | |||
branchLike, | |||
component: { | |||
...baseComponent, | |||
configuration: { showSettings }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
} | |||
}) | |||
).toMatchSnapshot() | |||
); | |||
}); | |||
@@ -119,17 +110,14 @@ it('should work for a branch', () => { | |||
it('should work for pull requests', () => { | |||
[true, false].forEach(showSettings => | |||
expect( | |||
shallow( | |||
<Menu | |||
appState={{ branchesEnabled: true }} | |||
branchLike={mockPullRequest()} | |||
component={{ | |||
...baseComponent, | |||
configuration: { showSettings }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
}} | |||
/> | |||
) | |||
shallowRender({ | |||
branchLike: mockPullRequest(), | |||
component: { | |||
...baseComponent, | |||
configuration: { showSettings }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
} | |||
}) | |||
).toMatchSnapshot() | |||
); | |||
}); | |||
@@ -145,10 +133,18 @@ it('should work for all qualifiers', () => { | |||
function checkWithQualifier(qualifier: string) { | |||
const component = { ...baseComponent, configuration: { showSettings: true }, qualifier }; | |||
expect( | |||
shallow( | |||
<Menu appState={{ branchesEnabled: true }} branchLike={mainBranch} component={component} /> | |||
) | |||
).toMatchSnapshot(); | |||
expect(shallowRender({ component })).toMatchSnapshot(); | |||
} | |||
}); | |||
function shallowRender(props: Partial<Menu['props']>) { | |||
return shallow<Menu>( | |||
<Menu | |||
appState={{ branchesEnabled: true }} | |||
branchLike={mainBranch} | |||
component={baseComponent} | |||
onToggleProjectInfo={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -2,11 +2,32 @@ | |||
exports[`renders 1`] = ` | |||
<ContextNavBar | |||
height={72} | |||
height={102} | |||
id="context-navigation" | |||
notif={ | |||
<ComponentNavBgTaskNotif | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [ | |||
Object { | |||
"key": "component", | |||
"name": "component", | |||
"qualifier": "TRK", | |||
}, | |||
], | |||
"key": "component", | |||
"name": "component", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
isInProgress={true} | |||
isPending={true} | |||
/> | |||
} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between little-padder-top padder-bottom" | |||
className="display-flex-center display-flex-space-between little-padded-top padded-bottom" | |||
> | |||
<Connect(Component) | |||
branchLikes={Array []} | |||
@@ -61,6 +82,31 @@ exports[`renders 1`] = ` | |||
"qualifier": "TRK", | |||
} | |||
} | |||
onToggleProjectInfo={[Function]} | |||
/> | |||
<InfoDrawer | |||
displayed={false} | |||
onClose={[Function]} | |||
top={120} | |||
> | |||
<Connect(ProjectInformation) | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [ | |||
Object { | |||
"key": "component", | |||
"name": "component", | |||
"qualifier": "TRK", | |||
}, | |||
], | |||
"key": "component", | |||
"name": "component", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</InfoDrawer> | |||
</ContextNavBar> | |||
`; |
@@ -77,6 +77,24 @@ exports[`should work for a branch 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -235,6 +253,19 @@ exports[`should work for a branch 1`] = ` | |||
> | |||
<Component /> | |||
</Dropdown> | |||
<li> | |||
<a | |||
className="menu-button" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<BulletListIcon | |||
className="little-spacer-right" | |||
/> | |||
project.info.title | |||
</a> | |||
</li> | |||
</NavBarTabs> | |||
</div> | |||
`; | |||
@@ -281,6 +312,24 @@ exports[`should work for a branch 2`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -336,7 +385,21 @@ exports[`should work for a branch 2`] = ` | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
<NavBarTabs /> | |||
<NavBarTabs> | |||
<li> | |||
<a | |||
className="menu-button" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<BulletListIcon | |||
className="little-spacer-right" | |||
/> | |||
project.info.title | |||
</a> | |||
</li> | |||
</NavBarTabs> | |||
</div> | |||
`; | |||
@@ -380,6 +443,23 @@ exports[`should work for all qualifiers 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -530,6 +610,19 @@ exports[`should work for all qualifiers 1`] = ` | |||
> | |||
<Component /> | |||
</Dropdown> | |||
<li> | |||
<a | |||
className="menu-button" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<BulletListIcon | |||
className="little-spacer-right" | |||
/> | |||
project.info.title | |||
</a> | |||
</li> | |||
</NavBarTabs> | |||
</div> | |||
`; | |||
@@ -796,6 +889,23 @@ exports[`should work for all qualifiers 4`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -878,6 +988,19 @@ exports[`should work for all qualifiers 4`] = ` | |||
> | |||
<Component /> | |||
</Dropdown> | |||
<li> | |||
<a | |||
className="menu-button" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<BulletListIcon | |||
className="little-spacer-right" | |||
/> | |||
application.info.title | |||
</a> | |||
</li> | |||
</NavBarTabs> | |||
</div> | |||
`; | |||
@@ -924,6 +1047,24 @@ exports[`should work for pull requests 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
"pullRequest": "1001", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1007,6 +1148,24 @@ exports[`should work for pull requests 2`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
"pullRequest": "1001", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -18,21 +18,27 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { ClipboardButton } from 'sonar-ui-common/components/controls/clipboard'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon'; | |||
interface Props { | |||
organization: string; | |||
export interface DrawerLinkProps<P> { | |||
label: string; | |||
onPageChange: (page: P) => void; | |||
to: P; | |||
} | |||
export default function MetaOrganizationKey({ organization }: Props) { | |||
export function DrawerLink<P>(props: DrawerLinkProps<P>) { | |||
const { label, to } = props; | |||
return ( | |||
<> | |||
<h4 className="overview-meta-header big-spacer-top">{translate('organization_key')}</h4> | |||
<div className="display-flex-center"> | |||
<input className="overview-key" readOnly={true} type="text" value={organization} /> | |||
<ClipboardButton className="little-spacer-left" copyValue={organization} /> | |||
</div> | |||
</> | |||
<a | |||
className="display-flex-space-between bordered-bottom big-padded" | |||
onClick={() => props.onPageChange(to)} | |||
role="link" | |||
tabIndex={0}> | |||
{label} | |||
<ChevronRightIcon /> | |||
</a> | |||
); | |||
} | |||
export default React.memo(DrawerLink); |
@@ -0,0 +1,70 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
:root { | |||
--drawer-width: 380px; | |||
} | |||
/* TODO: should we move this? Or handle it differently? */ | |||
.navbar-inner-with-notif .info-drawer { | |||
border-top: 1px solid var(--barBorderColor); | |||
} | |||
.info-drawer-pane { | |||
background-color: white; | |||
right: calc(-1 * var(--drawer-width)); | |||
width: var(--drawer-width); | |||
transition: right 0.3s ease-in-out; | |||
border-left: 1px solid var(--barBorderColor); | |||
box-sizing: border-box; | |||
} | |||
.info-drawer-pane.open { | |||
right: 0; | |||
} | |||
.info-drawer { | |||
position: fixed; | |||
/* top is defined programmatically by ComponentNav */ | |||
bottom: 0; | |||
z-index: var(--pageSideZIndex); | |||
} | |||
.info-drawer .close-button { | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
background: white; | |||
padding: calc(2 * var(--gridSize)); | |||
z-index: var(--normalZIndex); | |||
} | |||
.info-drawer .back-button { | |||
cursor: pointer; | |||
} | |||
.info-drawer .back-button:hover { | |||
color: var(--blue); | |||
} | |||
.info-drawer-page { | |||
position: absolute; | |||
top: 0; | |||
bottom: 0; | |||
} |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { ClearButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import EscKeydownHandler from 'sonar-ui-common/components/controls/EscKeydownHandler'; | |||
import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler'; | |||
import './InfoDrawer.css'; | |||
export interface InfoDrawerProps { | |||
children: React.ReactNode; | |||
displayed: boolean; | |||
onClose: () => void; | |||
top: number; | |||
} | |||
export default function InfoDrawer(props: InfoDrawerProps) { | |||
const { children, displayed, onClose, top } = props; | |||
return ( | |||
<div | |||
className={classNames('info-drawer info-drawer-pane', { open: displayed })} | |||
style={{ top }}> | |||
<div className="close-button"> | |||
<ClearButton onClick={onClose} /> | |||
</div> | |||
{displayed && ( | |||
<EscKeydownHandler onKeydown={onClose}> | |||
<OutsideClickHandler onClickOutside={onClose}> | |||
<div className="display-flex-column max-height-100">{children}</div> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import BackIcon from 'sonar-ui-common/components/icons/BackIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface InfoDrawerPageProps { | |||
children: React.ReactNode; | |||
displayed: boolean; | |||
onPageChange: () => void; | |||
} | |||
export default function InfoDrawerPage(props: InfoDrawerPageProps) { | |||
const { children, displayed, onPageChange } = props; | |||
return ( | |||
<div | |||
className={classNames( | |||
'info-drawer-page info-drawer-pane display-flex-column overflow-hidden', | |||
{ | |||
open: displayed | |||
} | |||
)}> | |||
<h2 className="back-button big-padded bordered-bottom" onClick={() => onPageChange()}> | |||
<BackIcon className="little-spacer-right" /> | |||
{translate('back')} | |||
</h2> | |||
{displayed && <div className="overflow-y-auto big-padded">{children}</div>} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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. | |||
*/ | |||
.project-info-list > li { | |||
/* 1px to not cut icons on the left */ | |||
padding-left: 1px; | |||
padding-bottom: 4px; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
} | |||
.project-info-tags { | |||
position: relative; | |||
} | |||
.project-info-deleted-profile, | |||
.project-info-deprecated-rules { | |||
margin: 4px -6px 4px; | |||
padding: 3px 6px !important; | |||
border: 1px solid var(--alertBorderError); | |||
border-radius: 3px; | |||
background-color: var(--alertBackgroundError); | |||
} | |||
.project-info-deleted-profile a, | |||
.project-info-deprecated-rules a { | |||
color: var(--veryDarkBlue); | |||
border-color: darken(var(--lightBlue)); | |||
} |
@@ -0,0 +1,137 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getMeasures } from '../../../../../api/measures'; | |||
import { isLoggedIn } from '../../../../../helpers/users'; | |||
import { fetchMetrics } from '../../../../../store/rootActions'; | |||
import { getCurrentUser, getMetrics, Store } from '../../../../../store/rootReducer'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { MetricKey } from '../../../../../types/metrics'; | |||
import ProjectBadges from './badges/ProjectBadges'; | |||
import InfoDrawerPage from './InfoDrawerPage'; | |||
import ProjectNotifications from './notifications/ProjectNotifications'; | |||
import './ProjectInformation.css'; | |||
import { ProjectInformationPages } from './ProjectInformationPages'; | |||
import ProjectInformationRenderer from './ProjectInformationRenderer'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: T.Component; | |||
currentUser: T.CurrentUser; | |||
fetchMetrics: () => void; | |||
onComponentChange: (changes: {}) => void; | |||
metrics: T.Dict<T.Metric>; | |||
} | |||
interface State { | |||
measures?: T.Measure[]; | |||
page: ProjectInformationPages; | |||
} | |||
export class ProjectInformation extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
page: ProjectInformationPages.main | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.props.fetchMetrics(); | |||
this.loadMeasures(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
setPage = (page: ProjectInformationPages = ProjectInformationPages.main) => { | |||
this.setState({ page }); | |||
}; | |||
loadMeasures = () => { | |||
const { | |||
component: { key } | |||
} = this.props; | |||
return getMeasures({ | |||
component: key, | |||
metricKeys: [MetricKey.ncloc, MetricKey.projects].join() | |||
}).then(measures => { | |||
if (this.mounted) { | |||
this.setState({ measures }); | |||
} | |||
}); | |||
}; | |||
render() { | |||
const { branchLike, component, currentUser, metrics } = this.props; | |||
const { measures, page } = this.state; | |||
const canConfigureNotifications = isLoggedIn(currentUser); | |||
const canUseBadges = | |||
metrics !== undefined && | |||
component.visibility !== 'private' && | |||
(component.qualifier === ComponentQualifier.Application || | |||
component.qualifier === ComponentQualifier.Project); | |||
return ( | |||
<> | |||
<ProjectInformationRenderer | |||
canConfigureNotifications={canConfigureNotifications} | |||
canUseBadges={canUseBadges} | |||
component={component} | |||
measures={measures} | |||
onComponentChange={this.props.onComponentChange} | |||
onPageChange={this.setPage} | |||
/> | |||
{canUseBadges && ( | |||
<InfoDrawerPage | |||
displayed={page === ProjectInformationPages.badges} | |||
onPageChange={this.setPage}> | |||
<ProjectBadges | |||
branchLike={branchLike} | |||
metrics={metrics} | |||
project={component.key} | |||
qualifier={component.qualifier} | |||
/> | |||
</InfoDrawerPage> | |||
)} | |||
{canConfigureNotifications && ( | |||
<InfoDrawerPage | |||
displayed={page === ProjectInformationPages.notifications} | |||
onPageChange={this.setPage}> | |||
<ProjectNotifications component={component} /> | |||
</InfoDrawerPage> | |||
)} | |||
</> | |||
); | |||
} | |||
} | |||
const mapDispatchToProps = { fetchMetrics }; | |||
const mapStateToProps = (state: Store) => ({ | |||
currentUser: getCurrentUser(state), | |||
metrics: getMetrics(state) | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectInformation); |
@@ -0,0 +1,24 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 enum ProjectInformationPages { | |||
main, | |||
badges, | |||
notifications | |||
} |
@@ -0,0 +1,123 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import DrawerLink from './DrawerLink'; | |||
import MetaKey from './meta/MetaKey'; | |||
import MetaLinks from './meta/MetaLinks'; | |||
import MetaQualityGate from './meta/MetaQualityGate'; | |||
import MetaQualityProfiles from './meta/MetaQualityProfiles'; | |||
import MetaSize from './meta/MetaSize'; | |||
import MetaTags from './meta/MetaTags'; | |||
import { ProjectInformationPages } from './ProjectInformationPages'; | |||
export interface ProjectInformationRendererProps { | |||
canConfigureNotifications: boolean; | |||
canUseBadges: boolean; | |||
component: T.Component; | |||
measures?: T.Measure[]; | |||
onComponentChange: (changes: {}) => void; | |||
onPageChange: (page: ProjectInformationPages) => void; | |||
} | |||
export function ProjectInformationRenderer(props: ProjectInformationRendererProps) { | |||
const { canConfigureNotifications, canUseBadges, component, measures = [] } = props; | |||
const isApp = component.qualifier === ComponentQualifier.Application; | |||
return ( | |||
<> | |||
<div> | |||
<h2 className="big-padded bordered-bottom"> | |||
{translate(isApp ? 'application' : 'project', 'info.title')} | |||
</h2> | |||
</div> | |||
<div className="overflow-y-auto"> | |||
{(component.description || !isApp) && ( | |||
<div className="big-padded bordered-bottom"> | |||
<div className="display-flex-center"> | |||
<h3 className="spacer-right">{translate('project.info.description')}</h3> | |||
{component.visibility && ( | |||
<PrivacyBadgeContainer | |||
organization={undefined} | |||
qualifier={component.qualifier} | |||
tooltipProps={{ projectKey: component.key }} | |||
visibility={component.visibility} | |||
/> | |||
)} | |||
</div> | |||
{component.description && <p className="spacer-bottom">{component.description}</p>} | |||
{!isApp && ( | |||
<MetaTags component={component} onComponentChange={props.onComponentChange} /> | |||
)} | |||
</div> | |||
)} | |||
<div className="big-padded bordered-bottom it__project-loc-value"> | |||
<MetaSize component={component} measures={measures} /> | |||
</div> | |||
{(component.qualityGate || | |||
(component.qualityProfiles && component.qualityProfiles.length > 0)) && ( | |||
<> | |||
<div className="big-padded bordered-bottom"> | |||
{component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />} | |||
{component.qualityProfiles && component.qualityProfiles.length > 0 && ( | |||
<MetaQualityProfiles | |||
headerClassName={component.qualityGate ? 'big-spacer-top' : undefined} | |||
profiles={component.qualityProfiles} | |||
/> | |||
)} | |||
</div> | |||
</> | |||
)} | |||
{!isApp && <MetaLinks component={component} />} | |||
<div className="big-padded bordered-bottom"> | |||
<MetaKey componentKey={component.key} qualifier={component.qualifier} /> | |||
</div> | |||
{canUseBadges && ( | |||
<DrawerLink | |||
label={translate('overview.badges.get_badge', component.qualifier)} | |||
onPageChange={props.onPageChange} | |||
to={ProjectInformationPages.badges} | |||
/> | |||
)} | |||
{canConfigureNotifications && ( | |||
<DrawerLink | |||
label={translate('project.info.to_notifications')} | |||
onPageChange={props.onPageChange} | |||
to={ProjectInformationPages.notifications} | |||
/> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} | |||
export default React.memo(ProjectInformationRenderer); |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { DrawerLink, DrawerLinkProps } from '../DrawerLink'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should call onPageChange when clicked', () => { | |||
const onPageChange = jest.fn(); | |||
const to = 'target'; | |||
const wrapper = shallowRender({ onPageChange, to }); | |||
wrapper.simulate('click'); | |||
expect(onPageChange).toBeCalledWith(to); | |||
}); | |||
function shallowRender(props: Partial<DrawerLinkProps<string>> = {}) { | |||
return shallow(<DrawerLink label="switch page" onPageChange={jest.fn()} to="id" {...props} />); | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 InfoDrawer, { InfoDrawerProps } from '../InfoDrawer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ displayed: true })).toMatchSnapshot('displayed'); | |||
}); | |||
it('should call onClose when button is clicked', () => { | |||
const onClose = jest.fn(); | |||
const wrapper = shallowRender({ onClose }); | |||
wrapper.find('ClearButton').simulate('click'); | |||
expect(onClose).toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<InfoDrawerProps> = {}) { | |||
return shallow( | |||
<InfoDrawer displayed={false} onClose={jest.fn()} top={120} {...props}> | |||
<span>content</span> | |||
</InfoDrawer> | |||
); | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 InfoDrawerPage, { InfoDrawerPageProps } from '../InfoDrawerPage'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ displayed: true })).toMatchSnapshot(); | |||
}); | |||
it('should call onPageChange when clicked', () => { | |||
const onPageChange = jest.fn(); | |||
const wrapper = shallowRender({ onPageChange }); | |||
wrapper.find('.back-button').simulate('click'); | |||
expect(onPageChange).toBeCalledTimes(1); | |||
}); | |||
function shallowRender(props: Partial<InfoDrawerPageProps> = {}) { | |||
return shallow( | |||
<InfoDrawerPage displayed={false} onPageChange={jest.fn()} {...props}> | |||
<div>content</div> | |||
</InfoDrawerPage> | |||
); | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
mockComponent, | |||
mockCurrentUser, | |||
mockLoggedInUser, | |||
mockMetric | |||
} from '../../../../../../helpers/testMocks'; | |||
import { ProjectInformation } from '../ProjectInformation'; | |||
import { ProjectInformationPages } from '../ProjectInformationPages'; | |||
jest.mock('../../../../../../api/measures', () => { | |||
const { mockMeasure } = jest.requireActual('../../../../../../helpers/testMocks'); | |||
return { | |||
getMeasures: jest.fn().mockResolvedValue([mockMeasure()]) | |||
}; | |||
}); | |||
it('should render correctly', async () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged in user'); | |||
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot( | |||
'private' | |||
); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot('measures loaded'); | |||
}); | |||
it('should handle page change', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().setPage(ProjectInformationPages.badges); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().page).toBe(ProjectInformationPages.badges); | |||
}); | |||
function shallowRender(props: Partial<ProjectInformation['props']> = {}) { | |||
return shallow<ProjectInformation>( | |||
<ProjectInformation | |||
component={mockComponent()} | |||
currentUser={mockCurrentUser()} | |||
fetchMetrics={jest.fn()} | |||
metrics={{ | |||
coverage: mockMetric() | |||
}} | |||
onComponentChange={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { mockComponent } from '../../../../../../helpers/testMocks'; | |||
import { | |||
ProjectInformationRenderer, | |||
ProjectInformationRendererProps | |||
} from '../ProjectInformationRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ canConfigureNotifications: false })).toMatchSnapshot('with notifications'); | |||
expect(shallowRender({ canUseBadges: false })).toMatchSnapshot('no badges'); | |||
expect(shallowRender({ canConfigureNotifications: false, canUseBadges: false })).toMatchSnapshot( | |||
'no badges, no notifications' | |||
); | |||
}); | |||
it('should render a private project correctly', () => { | |||
expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot(); | |||
}); | |||
it('should render an app correctly', () => { | |||
const component = mockComponent({ qualifier: 'APP' }); | |||
expect(shallowRender({ component })).toMatchSnapshot('default'); | |||
}); | |||
it('should handle missing quality profiles and quality gates', () => { | |||
expect( | |||
shallowRender({ | |||
component: mockComponent({ qualityGate: undefined, qualityProfiles: undefined }) | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<ProjectInformationRendererProps> = {}) { | |||
return shallow( | |||
<ProjectInformationRenderer | |||
canConfigureNotifications={true} | |||
canUseBadges={true} | |||
component={mockComponent({ qualifier: 'TRK', visibility: 'public' })} | |||
onComponentChange={jest.fn()} | |||
onPageChange={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,13 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<a | |||
className="display-flex-space-between bordered-bottom big-padded" | |||
onClick={[Function]} | |||
role="link" | |||
tabIndex={0} | |||
> | |||
switch page | |||
<ChevronRightIcon /> | |||
</a> | |||
`; |
@@ -0,0 +1,54 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="info-drawer info-drawer-pane" | |||
style={ | |||
Object { | |||
"top": 120, | |||
} | |||
} | |||
> | |||
<div | |||
className="close-button" | |||
> | |||
<ClearButton | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: displayed 1`] = ` | |||
<div | |||
className="info-drawer info-drawer-pane open" | |||
style={ | |||
Object { | |||
"top": 120, | |||
} | |||
} | |||
> | |||
<div | |||
className="close-button" | |||
> | |||
<ClearButton | |||
onClick={[MockFunction]} | |||
/> | |||
</div> | |||
<EscKeydownHandler | |||
onKeydown={[MockFunction]} | |||
> | |||
<OutsideClickHandler | |||
onClickOutside={[MockFunction]} | |||
> | |||
<div | |||
className="display-flex-column max-height-100" | |||
> | |||
<span> | |||
content | |||
</span> | |||
</div> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
</div> | |||
`; |
@@ -0,0 +1,40 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="info-drawer-page info-drawer-pane display-flex-column overflow-hidden" | |||
> | |||
<h2 | |||
className="back-button big-padded bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<BackIcon | |||
className="little-spacer-right" | |||
/> | |||
back | |||
</h2> | |||
</div> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<div | |||
className="info-drawer-page info-drawer-pane display-flex-column overflow-hidden open" | |||
> | |||
<h2 | |||
className="back-button big-padded bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<BackIcon | |||
className="little-spacer-right" | |||
/> | |||
back | |||
</h2> | |||
<div | |||
className="overflow-y-auto big-padded" | |||
> | |||
<div> | |||
content | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,241 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<Fragment> | |||
<Memo(ProjectInformationRenderer) | |||
canConfigureNotifications={false} | |||
canUseBadges={true} | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
onPageChange={[Function]} | |||
/> | |||
<InfoDrawerPage | |||
displayed={false} | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: logged in user 1`] = ` | |||
<Fragment> | |||
<Memo(ProjectInformationRenderer) | |||
canConfigureNotifications={true} | |||
canUseBadges={true} | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
onPageChange={[Function]} | |||
/> | |||
<InfoDrawerPage | |||
displayed={false} | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
<InfoDrawerPage | |||
displayed={false} | |||
onPageChange={[Function]} | |||
> | |||
<withNotifications(ProjectNotifications) | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: measures loaded 1`] = ` | |||
<Fragment> | |||
<Memo(ProjectInformationRenderer) | |||
canConfigureNotifications={false} | |||
canUseBadges={true} | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
measures={ | |||
Array [ | |||
Object { | |||
"bestValue": true, | |||
"metric": "bugs", | |||
"periods": Array [ | |||
Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
], | |||
"value": "1.0", | |||
}, | |||
] | |||
} | |||
onComponentChange={[MockFunction]} | |||
onPageChange={[Function]} | |||
/> | |||
<InfoDrawerPage | |||
displayed={false} | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: private 1`] = ` | |||
<Fragment> | |||
<Memo(ProjectInformationRenderer) | |||
canConfigureNotifications={false} | |||
canUseBadges={false} | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "private", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
onPageChange={[Function]} | |||
/> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,996 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should handle missing quality profiles and quality gates 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": undefined, | |||
"qualityProfiles": undefined, | |||
"tags": Array [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": undefined, | |||
"qualityProfiles": undefined, | |||
"tags": Array [], | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": undefined, | |||
"qualityProfiles": undefined, | |||
"tags": Array [], | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="overview.badges.get_badge.TRK" | |||
onPageChange={[MockFunction]} | |||
to={1} | |||
/> | |||
<Memo(DrawerLink) | |||
label="project.info.to_notifications" | |||
onPageChange={[MockFunction]} | |||
to={2} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render a private project correctly 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
<Connect(PrivacyBadge) | |||
qualifier="TRK" | |||
tooltipProps={ | |||
Object { | |||
"projectKey": "my-project", | |||
} | |||
} | |||
visibility="private" | |||
/> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "private", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "private", | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "private", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="overview.badges.get_badge.TRK" | |||
onPageChange={[MockFunction]} | |||
to={1} | |||
/> | |||
<Memo(DrawerLink) | |||
label="project.info.to_notifications" | |||
onPageChange={[MockFunction]} | |||
to={2} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render an app correctly: default 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
application.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "APP", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="APP" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="overview.badges.get_badge.APP" | |||
onPageChange={[MockFunction]} | |||
to={1} | |||
/> | |||
<Memo(DrawerLink) | |||
label="project.info.to_notifications" | |||
onPageChange={[MockFunction]} | |||
to={2} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: default 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
<Connect(PrivacyBadge) | |||
qualifier="TRK" | |||
tooltipProps={ | |||
Object { | |||
"projectKey": "my-project", | |||
} | |||
} | |||
visibility="public" | |||
/> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="overview.badges.get_badge.TRK" | |||
onPageChange={[MockFunction]} | |||
to={1} | |||
/> | |||
<Memo(DrawerLink) | |||
label="project.info.to_notifications" | |||
onPageChange={[MockFunction]} | |||
to={2} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: no badges 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
<Connect(PrivacyBadge) | |||
qualifier="TRK" | |||
tooltipProps={ | |||
Object { | |||
"projectKey": "my-project", | |||
} | |||
} | |||
visibility="public" | |||
/> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="project.info.to_notifications" | |||
onPageChange={[MockFunction]} | |||
to={2} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: no badges, no notifications 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
<Connect(PrivacyBadge) | |||
qualifier="TRK" | |||
tooltipProps={ | |||
Object { | |||
"projectKey": "my-project", | |||
} | |||
} | |||
visibility="public" | |||
/> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: with notifications 1`] = ` | |||
<Fragment> | |||
<div> | |||
<h2 | |||
className="big-padded bordered-bottom" | |||
> | |||
project.info.title | |||
</h2> | |||
</div> | |||
<div | |||
className="overflow-y-auto" | |||
> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<h3 | |||
className="spacer-right" | |||
> | |||
project.info.description | |||
</h3> | |||
<Connect(PrivacyBadge) | |||
qualifier="TRK" | |||
tooltipProps={ | |||
Object { | |||
"projectKey": "my-project", | |||
} | |||
} | |||
visibility="public" | |||
/> | |||
</div> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom it__project-loc-value" | |||
> | |||
<MetaSize | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
measures={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaQualityGate | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
"visibility": "public", | |||
} | |||
} | |||
/> | |||
<div | |||
className="big-padded bordered-bottom" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
</div> | |||
<Memo(DrawerLink) | |||
label="overview.badges.get_badge.TRK" | |||
onPageChange={[MockFunction]} | |||
to={1} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -21,7 +21,7 @@ import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { fetchWebApi } from '../../../api/web-api'; | |||
import { fetchWebApi } from '../../../../../../api/web-api'; | |||
import { BadgeColors, BadgeFormats, BadgeOptions, BadgeType } from './utils'; | |||
interface Props { | |||
@@ -54,9 +54,9 @@ export default class BadgeParams extends React.PureComponent<Props> { | |||
fetchWebApi(false).then( | |||
webservices => { | |||
if (this.mounted) { | |||
const domain = webservices.find(domain => domain.path === 'api/project_badges'); | |||
const ws = domain && domain.actions.find(ws => ws.key === 'measure'); | |||
const param = ws && ws.params && ws.params.find(param => param.key === 'metric'); | |||
const domain = webservices.find(d => d.path === 'api/project_badges'); | |||
const ws = domain && domain.actions.find(w => w.key === 'measure'); | |||
const param = ws && ws.params && ws.params.find(p => p.key === 'metric'); | |||
if (param && param.possibleValues) { | |||
this.setState({ badgeMetrics: param.possibleValues }); | |||
} | |||
@@ -129,6 +129,7 @@ export default class BadgeParams extends React.PureComponent<Props> { | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
menuStyle={{ maxHeight: 100 }} | |||
name="badge-metric" | |||
onChange={this.handleMetricChange} | |||
options={this.getMetricOptions()} | |||
@@ -150,7 +151,7 @@ export default class BadgeParams extends React.PureComponent<Props> { | |||
<label | |||
className={classNames('spacer-right', { | |||
'big-spacer-left': type !== BadgeType.qualityGate | |||
'spacer-top': type !== BadgeType.qualityGate | |||
})} | |||
htmlFor="badge-format"> | |||
{translate('format')}: |
@@ -0,0 +1,95 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import CodeSnippet from '../../../../../../components/common/CodeSnippet'; | |||
import { getBranchLikeQuery } from '../../../../../../helpers/branch-like'; | |||
import { BranchLike } from '../../../../../../types/branch-like'; | |||
import { MetricKey } from '../../../../../../types/metrics'; | |||
import BadgeButton from './BadgeButton'; | |||
import BadgeParams from './BadgeParams'; | |||
import './styles.css'; | |||
import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
metrics: T.Dict<T.Metric>; | |||
project: string; | |||
qualifier: string; | |||
} | |||
interface State { | |||
selectedType: BadgeType; | |||
badgeOptions: BadgeOptions; | |||
} | |||
export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
state: State = { | |||
selectedType: BadgeType.measure, | |||
badgeOptions: { color: 'white', metric: MetricKey.alert_status } | |||
}; | |||
handleSelectBadge = (selectedType: BadgeType) => { | |||
this.setState({ selectedType }); | |||
}; | |||
handleUpdateOptions = (options: Partial<BadgeOptions>) => { | |||
this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } })); | |||
}; | |||
render() { | |||
const { branchLike, project, qualifier } = this.props; | |||
const { selectedType, badgeOptions } = this.state; | |||
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) }; | |||
return ( | |||
<div className="display-flex-column"> | |||
<h3>{translate('overview.badges.get_badge', qualifier)}</h3> | |||
<p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p> | |||
<BadgeButton | |||
onClick={this.handleSelectBadge} | |||
selected={BadgeType.measure === selectedType} | |||
type={BadgeType.measure} | |||
url={getBadgeUrl(BadgeType.measure, fullBadgeOptions)} | |||
/> | |||
<p className="huge-spacer-bottom spacer-top"> | |||
{translate('overview.badges', BadgeType.measure, 'description', qualifier)} | |||
</p> | |||
<BadgeButton | |||
onClick={this.handleSelectBadge} | |||
selected={BadgeType.qualityGate === selectedType} | |||
type={BadgeType.qualityGate} | |||
url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions)} | |||
/> | |||
<p className="huge-spacer-bottom spacer-top"> | |||
{translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)} | |||
</p> | |||
<BadgeParams | |||
className="big-spacer-bottom display-flex-column" | |||
metrics={this.props.metrics} | |||
options={badgeOptions} | |||
type={selectedType} | |||
updateOptions={this.handleUpdateOptions} | |||
/> | |||
<CodeSnippet isOneLine={true} snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} /> | |||
</div> | |||
); | |||
} | |||
} |
@@ -22,7 +22,7 @@ import * as React from 'react'; | |||
import BadgeParams from '../BadgeParams'; | |||
import { BadgeType } from '../utils'; | |||
jest.mock('../../../../api/web-api', () => ({ | |||
jest.mock('../../../../../../../api/web-api', () => ({ | |||
fetchWebApi: () => | |||
Promise.resolve([ | |||
{ |
@@ -19,10 +19,10 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { Location } from 'sonar-ui-common/helpers/urls'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { mockBranch } from '../../../../../../../helpers/mocks/branch-like'; | |||
import { mockMetric } from '../../../../../../../helpers/testMocks'; | |||
import { MetricKey } from '../../../../../../../types/metrics'; | |||
import ProjectBadges from '../ProjectBadges'; | |||
jest.mock('sonar-ui-common/helpers/urls', () => ({ | |||
@@ -30,30 +30,25 @@ jest.mock('sonar-ui-common/helpers/urls', () => ({ | |||
getPathUrlAsString: (l: Location) => l.pathname | |||
})); | |||
jest.mock('../../../../helpers/urls', () => ({ | |||
jest.mock('../../../../../../../helpers/urls', () => ({ | |||
getProjectUrl: () => ({ pathname: '/dashboard' } as Location) | |||
})); | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
const shortBranch = mockBranch({ name: 'branch-6.6' }); | |||
it('should display the modal after click on sonarcloud', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => true); | |||
const wrapper = shallow( | |||
<ProjectBadges branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('Button')); | |||
expect(wrapper.find('Modal')).toMatchSnapshot(); | |||
it('should display correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should display the modal after click on sonarqube', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => false); | |||
const wrapper = shallow( | |||
<ProjectBadges branchLike={shortBranch} metrics={{}} project="foo" qualifier="TRK" /> | |||
function shallowRender(overrides = {}) { | |||
return shallow( | |||
<ProjectBadges | |||
branchLike={mockBranch()} | |||
metrics={{ | |||
[MetricKey.coverage]: mockMetric({ key: MetricKey.coverage }), | |||
[MetricKey.new_code_smells]: mockMetric({ key: MetricKey.new_code_smells }) | |||
}} | |||
project="foo" | |||
qualifier="TRK" | |||
{...overrides} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('Button')); | |||
expect(wrapper.find('Modal')).toMatchSnapshot(); | |||
}); | |||
} |
@@ -34,7 +34,7 @@ exports[`should display marketing badge params 1`] = ` | |||
value="white" | |||
/> | |||
<label | |||
className="spacer-right big-spacer-left" | |||
className="spacer-right spacer-top" | |||
htmlFor="badge-format" | |||
> | |||
format | |||
@@ -75,6 +75,11 @@ exports[`should display measure badge params 1`] = ` | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
menuStyle={ | |||
Object { | |||
"maxHeight": 100, | |||
} | |||
} | |||
name="badge-metric" | |||
onChange={[Function]} | |||
options={Array []} | |||
@@ -82,7 +87,7 @@ exports[`should display measure badge params 1`] = ` | |||
value="alert_status" | |||
/> | |||
<label | |||
className="spacer-right big-spacer-left" | |||
className="spacer-right spacer-top" | |||
htmlFor="badge-format" | |||
> | |||
format |
@@ -0,0 +1,69 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correctly 1`] = ` | |||
<div | |||
className="display-flex-column" | |||
> | |||
<h3> | |||
overview.badges.get_badge.TRK | |||
</h3> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
overview.badges.description.TRK | |||
</p> | |||
<BadgeButton | |||
onClick={[Function]} | |||
selected={true} | |||
type="measure" | |||
url="host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status" | |||
/> | |||
<p | |||
className="huge-spacer-bottom spacer-top" | |||
> | |||
overview.badges.measure.description.TRK | |||
</p> | |||
<BadgeButton | |||
onClick={[Function]} | |||
selected={false} | |||
type="quality_gate" | |||
url="host/api/project_badges/quality_gate?branch=branch-6.7&project=foo" | |||
/> | |||
<p | |||
className="huge-spacer-bottom spacer-top" | |||
> | |||
overview.badges.quality_gate.description.TRK | |||
</p> | |||
<BadgeParams | |||
className="big-spacer-bottom display-flex-column" | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"new_code_smells": Object { | |||
"id": "new_code_smells", | |||
"key": "new_code_smells", | |||
"name": "New_code_smells", | |||
"type": "PERCENT", | |||
}, | |||
} | |||
} | |||
options={ | |||
Object { | |||
"color": "white", | |||
"metric": "alert_status", | |||
} | |||
} | |||
type="measure" | |||
updateOptions={[Function]} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status)](/dashboard)" | |||
/> | |||
</div> | |||
`; |
@@ -24,7 +24,7 @@ | |||
flex-wrap: nowrap; | |||
} | |||
.badge-button { | |||
.button.badge-button { | |||
display: flex; | |||
justify-content: center; | |||
padding: var(--gridSize); | |||
@@ -36,15 +36,15 @@ | |||
transition: all 0.3s ease; | |||
} | |||
.badge-button:hover, | |||
.badge-button:focus, | |||
.badge-button:active { | |||
.button.badge-button:hover, | |||
.button.badge-button:focus, | |||
.button.badge-button:active { | |||
background-color: var(--barBackgroundColor); | |||
border-color: var(--blue); | |||
box-shadow: none; | |||
} | |||
.badge-button.selected { | |||
.button.badge-button.selected { | |||
background-color: var(--lightBlue); | |||
border-color: var(--darkBlue); | |||
} |
@@ -21,7 +21,7 @@ import { stringify } from 'querystring'; | |||
import { getLocalizedMetricName } from 'sonar-ui-common/helpers/l10n'; | |||
import { omitNil } from 'sonar-ui-common/helpers/request'; | |||
import { getHostUrl, getPathUrlAsString } from 'sonar-ui-common/helpers/urls'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { getProjectUrl } from '../../../../../../helpers/urls'; | |||
export type BadgeColors = 'white' | 'black' | 'orange'; | |||
export type BadgeFormats = 'md' | 'url'; |
@@ -29,7 +29,7 @@ interface Props { | |||
export default function MetaKey({ componentKey, qualifier }: Props) { | |||
return ( | |||
<> | |||
<h4 className="overview-meta-header">{translate('overview.project_key', qualifier)}</h4> | |||
<h3>{translate('overview.project_key', qualifier)}</h3> | |||
<div className="display-flex-center"> | |||
<input className="overview-key" readOnly={true} type="text" value={componentKey} /> | |||
<ClipboardButton className="little-spacer-left" copyValue={componentKey} /> |
@@ -20,8 +20,8 @@ | |||
import * as React from 'react'; | |||
import { ClearButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import ProjectLinkIcon from 'sonar-ui-common/components/icons/ProjectLinkIcon'; | |||
import isValidUri from '../../../app/utils/isValidUri'; | |||
import { getLinkName } from '../../projectLinks/utils'; | |||
import { getLinkName } from '../../../../../../helpers/projectLinks'; | |||
import isValidUri from '../../../../../utils/isValidUri'; | |||
interface Props { | |||
iconOnly?: boolean; |
@@ -19,8 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getProjectLinks } from '../../../api/projectLinks'; | |||
import { orderLinks } from '../../projectLinks/utils'; | |||
import { getProjectLinks } from '../../../../../../api/projectLinks'; | |||
import { orderLinks } from '../../../../../../helpers/projectLinks'; | |||
import MetaLink from './MetaLink'; | |||
interface Props { | |||
@@ -70,14 +70,16 @@ export default class MetaLinks extends React.PureComponent<Props, State> { | |||
const orderedLinks = orderLinks(links); | |||
return ( | |||
<div className="overview-meta-card"> | |||
<h4 className="overview-meta-header">{translate('overview.external_links')}</h4> | |||
<ul className="overview-meta-list"> | |||
{orderedLinks.map(link => ( | |||
<MetaLink key={link.id} link={link} /> | |||
))} | |||
</ul> | |||
</div> | |||
<> | |||
<div className="big-padded bordered-bottom"> | |||
<h3>{translate('overview.external_links')}</h3> | |||
<ul className="project-info-list"> | |||
{orderedLinks.map(link => ( | |||
<MetaLink key={link.id} link={link} /> | |||
))} | |||
</ul> | |||
</div> | |||
</> | |||
); | |||
} | |||
} |
@@ -20,24 +20,23 @@ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getQualityGateUrl } from '../../../helpers/urls'; | |||
import { getQualityGateUrl } from '../../../../../../helpers/urls'; | |||
interface Props { | |||
organization?: string; | |||
qualityGate: { isDefault?: boolean; key: string; name: string }; | |||
} | |||
export default function MetaQualityGate({ qualityGate, organization }: Props) { | |||
export default function MetaQualityGate({ qualityGate }: Props) { | |||
return ( | |||
<> | |||
<h4 className="overview-meta-header">{translate('overview.quality_gate')}</h4> | |||
<h3>{translate('project.info.quality_gate')}</h3> | |||
<ul className="overview-meta-list"> | |||
<ul className="project-info-list"> | |||
<li> | |||
{qualityGate.isDefault && ( | |||
<span className="note spacer-right">{'(' + translate('default') + ')'}</span> | |||
<span className="note spacer-right">({translate('default')})</span> | |||
)} | |||
<Link to={getQualityGateUrl(qualityGate.key, organization)}>{qualityGate.name}</Link> | |||
<Link to={getQualityGateUrl(qualityGate.key)}>{qualityGate.name}</Link> | |||
</li> | |||
</ul> | |||
</> |
@@ -17,15 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { searchRules } from '../../../api/rules'; | |||
import { getQualityProfileUrl } from '../../../helpers/urls'; | |||
import { getLanguages, Store } from '../../../store/rootReducer'; | |||
import { searchRules } from '../../../../../../api/rules'; | |||
import { getQualityProfileUrl } from '../../../../../../helpers/urls'; | |||
import { getLanguages, Store } from '../../../../../../store/rootReducer'; | |||
interface StateProps { | |||
languages: T.Languages; | |||
@@ -33,7 +32,6 @@ interface StateProps { | |||
interface OwnProps { | |||
headerClassName?: string; | |||
organization?: string; | |||
profiles: T.ComponentQualityProfile[]; | |||
} | |||
@@ -77,7 +75,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro | |||
loadDeprecatedRulesForProfile(profileKey: string) { | |||
const data = { | |||
activation: 'true', | |||
organization: this.props.organization, | |||
organization: undefined, | |||
ps: 1, | |||
qprofile: profileKey, | |||
statuses: 'DEPRECATED' | |||
@@ -96,13 +94,11 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro | |||
const inner = ( | |||
<div className="text-ellipsis"> | |||
<span className="note spacer-right">{'(' + languageName + ')'}</span> | |||
<span className="note spacer-right">({languageName})</span> | |||
{profile.deleted ? ( | |||
profile.name | |||
) : ( | |||
<Link to={getQualityProfileUrl(profile.name, profile.language, this.props.organization)}> | |||
{profile.name} | |||
</Link> | |||
<Link to={getQualityProfileUrl(profile.name, profile.language)}>{profile.name}</Link> | |||
)} | |||
</div> | |||
); | |||
@@ -111,7 +107,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro | |||
const tooltip = translateWithParameters('overview.deleted_profile', profile.name); | |||
return ( | |||
<Tooltip key={profile.key} overlay={tooltip}> | |||
<li className="overview-deleted-profile">{inner}</li> | |||
<li className="project-info-deleted-profile">{inner}</li> | |||
</Tooltip> | |||
); | |||
} | |||
@@ -122,7 +118,7 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro | |||
const tooltip = translateWithParameters('overview.deprecated_profile', count); | |||
return ( | |||
<Tooltip key={profile.key} overlay={tooltip}> | |||
<li className="overview-deprecated-rules">{inner}</li> | |||
<li className="project-info-deprecated-rules">{inner}</li> | |||
</Tooltip> | |||
); | |||
} | |||
@@ -135,11 +131,9 @@ export class MetaQualityProfiles extends React.PureComponent<StateProps & OwnPro | |||
return ( | |||
<> | |||
<h4 className={classNames('overview-meta-header', headerClassName)}> | |||
{translate('overview.quality_profiles')} | |||
</h4> | |||
<h3 className={headerClassName}>{translate('overview.quality_profiles')}</h3> | |||
<ul className="overview-meta-list"> | |||
<ul className="project-info-list"> | |||
{profiles.map(profile => this.renderProfile(profile))} | |||
</ul> | |||
</> |
@@ -0,0 +1,75 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import SizeRating from 'sonar-ui-common/components/ui/SizeRating'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure, localizeMetric } from 'sonar-ui-common/helpers/measures'; | |||
import DrilldownLink from '../../../../../../components/shared/DrilldownLink'; | |||
import { ComponentQualifier } from '../../../../../../types/component'; | |||
import { MetricKey } from '../../../../../../types/metrics'; | |||
export interface MetaSizeProps { | |||
component: T.Component; | |||
measures: T.Measure[]; | |||
} | |||
export default function MetaSize({ component, measures }: MetaSizeProps) { | |||
const isApp = component.qualifier === ComponentQualifier.Application; | |||
const ncloc = measures.find(measure => measure.metric === MetricKey.ncloc); | |||
const projects = isApp | |||
? measures.find(measure => measure.metric === MetricKey.projects) | |||
: undefined; | |||
return ( | |||
<> | |||
<h3>{localizeMetric(MetricKey.ncloc)}</h3> | |||
<div className="display-flex-center"> | |||
{ncloc ? ( | |||
<> | |||
<DrilldownLink className="huge" component={component.key} metric={MetricKey.ncloc}> | |||
{formatMeasure(ncloc.value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
<span className="spacer-left"> | |||
<SizeRating value={Number(ncloc.value)} /> | |||
</span> | |||
</> | |||
) : ( | |||
<span>0</span> | |||
)} | |||
{isApp && ( | |||
<span className="huge-spacer-left display-inline-flex-center"> | |||
{projects ? ( | |||
<DrilldownLink component={component.key} metric={MetricKey.projects}> | |||
<span className="big">{formatMeasure(projects.value, 'SHORT_INT')}</span> | |||
</DrilldownLink> | |||
) : ( | |||
<span className="big">0</span> | |||
)} | |||
<span className="little-spacer-left text-muted"> | |||
{translate('metric.projects.name')} | |||
</span> | |||
</span> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} |
@@ -22,8 +22,8 @@ import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; | |||
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { setProjectTags } from '../../../api/components'; | |||
import TagsList from '../../../components/tags/TagsList'; | |||
import { setProjectTags } from '../../../../../../api/components'; | |||
import TagsList from '../../../../../../components/tags/TagsList'; | |||
import MetaTagsSelector from './MetaTagsSelector'; | |||
interface Props { | |||
@@ -62,7 +62,7 @@ export default class MetaTags extends React.PureComponent<Props> { | |||
if (this.canUpdateTags()) { | |||
return ( | |||
<div className="big-spacer-top overview-meta-tags" ref={card => (this.card = card)}> | |||
<div className="project-info-tags" ref={card => (this.card = card)}> | |||
<Dropdown | |||
closeOnClick={false} | |||
closeOnClickOutside={true} | |||
@@ -82,7 +82,7 @@ export default class MetaTags extends React.PureComponent<Props> { | |||
); | |||
} else { | |||
return ( | |||
<div className="big-spacer-top overview-meta-tags"> | |||
<div className="big-spacer-top project-info-tags"> | |||
<TagsList | |||
allowUpdate={false} | |||
className="note" |
@@ -19,8 +19,8 @@ | |||
*/ | |||
import { difference, without } from 'lodash'; | |||
import * as React from 'react'; | |||
import { searchProjectTags } from '../../../api/components'; | |||
import TagsSelector from '../../../components/tags/TagsSelector'; | |||
import { searchProjectTags } from '../../../../../../api/components'; | |||
import TagsSelector from '../../../../../../components/tags/TagsSelector'; | |||
interface Props { | |||
project: string; |
@@ -20,11 +20,11 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { searchRules } from '../../../../api/rules'; | |||
import { mockLanguage, mockQualityProfile } from '../../../../helpers/testMocks'; | |||
import { searchRules } from '../../../../../../../api/rules'; | |||
import { mockLanguage, mockQualityProfile } from '../../../../../../../helpers/testMocks'; | |||
import { MetaQualityProfiles } from '../MetaQualityProfiles'; | |||
jest.mock('../../../../api/rules', () => { | |||
jest.mock('../../../../../../../api/rules', () => { | |||
return { | |||
searchRules: jest.fn().mockResolvedValue({ | |||
total: 10 | |||
@@ -38,8 +38,8 @@ it('should render correctly', async () => { | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('.overview-deprecated-rules').exists()).toBe(true); | |||
expect(wrapper.find('.overview-deleted-profile').exists()).toBe(true); | |||
expect(wrapper.find('.project-info-deprecated-rules').exists()).toBe(true); | |||
expect(wrapper.find('.project-info-deleted-profile').exists()).toBe(true); | |||
expect(searchRules).toBeCalled(); | |||
}); | |||
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { mockComponent, mockMeasure } from '../../../../../../../helpers/testMocks'; | |||
import { ComponentQualifier } from '../../../../../../../types/component'; | |||
import { MetricKey } from '../../../../../../../types/metrics'; | |||
import MetaSize, { MetaSizeProps } from '../MetaSize'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('project'); | |||
expect( | |||
shallowRender({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }) | |||
).toMatchSnapshot('application'); | |||
}); | |||
function shallowRender(props: Partial<MetaSizeProps> = {}) { | |||
return shallow<MetaSizeProps>( | |||
<MetaSize | |||
component={mockComponent()} | |||
measures={[ | |||
mockMeasure({ metric: MetricKey.ncloc }), | |||
mockMeasure({ metric: MetricKey.projects }) | |||
]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../helpers/testMocks'; | |||
import { mockComponent } from '../../../../../../../helpers/testMocks'; | |||
import MetaTags from '../MetaTags'; | |||
const component = mockComponent({ |
@@ -20,10 +20,10 @@ | |||
/* eslint-disable import/first */ | |||
import { mount, shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { searchProjectTags } from '../../../../api/components'; | |||
import { searchProjectTags } from '../../../../../../../api/components'; | |||
import MetaTagsSelector from '../MetaTagsSelector'; | |||
jest.mock('../../../../api/components', () => ({ | |||
jest.mock('../../../../../../../api/components', () => ({ | |||
searchProjectTags: jest.fn() | |||
})); | |||
@@ -2,20 +2,18 @@ | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<h4 | |||
className="overview-meta-header" | |||
> | |||
<h3> | |||
overview.quality_profiles | |||
</h4> | |||
</h3> | |||
<ul | |||
className="overview-meta-list" | |||
className="project-info-list" | |||
> | |||
<Tooltip | |||
key="js" | |||
overlay="overview.deleted_profile.name" | |||
> | |||
<li | |||
className="overview-deleted-profile" | |||
className="project-info-deleted-profile" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
@@ -23,7 +21,9 @@ exports[`should render correctly 1`] = ` | |||
<span | |||
className="note spacer-right" | |||
> | |||
(js) | |||
( | |||
js | |||
) | |||
</span> | |||
name | |||
</div> | |||
@@ -38,7 +38,9 @@ exports[`should render correctly 1`] = ` | |||
<span | |||
className="note spacer-right" | |||
> | |||
(CSS) | |||
( | |||
CSS | |||
) | |||
</span> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
@@ -63,20 +65,18 @@ exports[`should render correctly 1`] = ` | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<h4 | |||
className="overview-meta-header" | |||
> | |||
<h3> | |||
overview.quality_profiles | |||
</h4> | |||
</h3> | |||
<ul | |||
className="overview-meta-list" | |||
className="project-info-list" | |||
> | |||
<Tooltip | |||
key="js" | |||
overlay="overview.deleted_profile.name" | |||
> | |||
<li | |||
className="overview-deleted-profile" | |||
className="project-info-deleted-profile" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
@@ -84,7 +84,9 @@ exports[`should render correctly 2`] = ` | |||
<span | |||
className="note spacer-right" | |||
> | |||
(js) | |||
( | |||
js | |||
) | |||
</span> | |||
name | |||
</div> | |||
@@ -95,7 +97,7 @@ exports[`should render correctly 2`] = ` | |||
overlay="overview.deprecated_profile.10" | |||
> | |||
<li | |||
className="overview-deprecated-rules" | |||
className="project-info-deprecated-rules" | |||
> | |||
<div | |||
className="text-ellipsis" | |||
@@ -103,7 +105,9 @@ exports[`should render correctly 2`] = ` | |||
<span | |||
className="note spacer-right" | |||
> | |||
(CSS) | |||
( | |||
CSS | |||
) | |||
</span> | |||
<Link | |||
onlyActiveOnIndex={false} |
@@ -0,0 +1,72 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: application 1`] = ` | |||
<Fragment> | |||
<h3> | |||
metric.ncloc.name | |||
</h3> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DrilldownLink | |||
className="huge" | |||
component="my-project" | |||
metric="ncloc" | |||
> | |||
1 | |||
</DrilldownLink> | |||
<span | |||
className="spacer-left" | |||
> | |||
<SizeRating | |||
value={1} | |||
/> | |||
</span> | |||
<span | |||
className="huge-spacer-left display-inline-flex-center" | |||
> | |||
<DrilldownLink | |||
component="my-project" | |||
metric="projects" | |||
> | |||
<span | |||
className="big" | |||
> | |||
1 | |||
</span> | |||
</DrilldownLink> | |||
<span | |||
className="little-spacer-left text-muted" | |||
> | |||
metric.projects.name | |||
</span> | |||
</span> | |||
</div> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: project 1`] = ` | |||
<Fragment> | |||
<h3> | |||
metric.ncloc.name | |||
</h3> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<DrilldownLink | |||
className="huge" | |||
component="my-project" | |||
metric="ncloc" | |||
> | |||
1 | |||
</DrilldownLink> | |||
<span | |||
className="spacer-left" | |||
> | |||
<SizeRating | |||
value={1} | |||
/> | |||
</span> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -2,7 +2,7 @@ | |||
exports[`should render with tags and admin rights 1`] = ` | |||
<div | |||
className="big-spacer-top overview-meta-tags" | |||
className="project-info-tags" | |||
> | |||
<Dropdown | |||
closeOnClick={false} | |||
@@ -41,7 +41,7 @@ exports[`should render with tags and admin rights 1`] = ` | |||
exports[`should render without tags and admin rights 1`] = ` | |||
<div | |||
className="big-spacer-top overview-meta-tags" | |||
className="big-spacer-top project-info-tags" | |||
> | |||
<TagsList | |||
allowUpdate={false} |
@@ -0,0 +1,92 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import NotificationsList from '../../../../../../apps/account/notifications/NotificationsList'; | |||
import { | |||
withNotifications, | |||
WithNotificationsProps | |||
} from '../../../../../../components/hoc/withNotifications'; | |||
interface Props { | |||
className?: string; | |||
component: T.Component; | |||
} | |||
export function ProjectNotifications(props: WithNotificationsProps & Props) { | |||
const { channels, component, loading, notifications, perProjectTypes } = props; | |||
const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => { | |||
props.addNotification({ project: component.key, channel, type }); | |||
}; | |||
const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => { | |||
props.removeNotification({ | |||
project: component.key, | |||
channel, | |||
type | |||
}); | |||
}; | |||
const getCheckboxId = (type: string, channel: string) => { | |||
return `project-notification-${component.key}-${type}-${channel}`; | |||
}; | |||
const projectNotifications = notifications.filter(n => n.project && n.project === component.key); | |||
return ( | |||
<> | |||
<h3>{translate('project.info.notifications')}</h3> | |||
<Alert className="spacer-top" variant="info"> | |||
{translate('notification.dispatcher.information')} | |||
</Alert> | |||
<DeferredSpinner loading={loading}> | |||
<table className="data zebra notifications-table"> | |||
<thead> | |||
<tr> | |||
<th aria-label={translate('project')} /> | |||
{channels.map(channel => ( | |||
<th className="text-center" key={channel}> | |||
<h4>{translate('notification.channel', channel)}</h4> | |||
</th> | |||
))} | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={channels} | |||
checkboxId={getCheckboxId} | |||
notifications={projectNotifications} | |||
onAdd={handleAddNotification} | |||
onRemove={handleRemoveNotification} | |||
project={true} | |||
types={perProjectTypes} | |||
/> | |||
</table> | |||
</DeferredSpinner> | |||
</> | |||
); | |||
} | |||
export default withNotifications(ProjectNotifications); |
@@ -17,9 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable sonarjs/no-duplicate-string */ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../helpers/testMocks'; | |||
import { mockComponent } from '../../../../../../../helpers/testMocks'; | |||
import { ProjectNotifications } from '../ProjectNotifications'; | |||
it('should render correctly', () => { | |||
@@ -43,7 +44,7 @@ it('should add and remove a notification for the project', () => { | |||
}); | |||
function shallowRender(props = {}) { | |||
const wrapper = shallow( | |||
return shallow( | |||
<ProjectNotifications | |||
addNotification={jest.fn()} | |||
channels={['channel1', 'channel2']} | |||
@@ -78,12 +79,4 @@ function shallowRender(props = {}) { | |||
{...props} | |||
/> | |||
); | |||
// Get the modal element. We need to trigger the ModalButton's `modal` prop, | |||
// which is a function. It will return our Modal component. | |||
return shallow( | |||
wrapper.find('ModalButton').prop<Function>('modal')({ | |||
onClose: jest.fn() | |||
}) | |||
); | |||
} |
@@ -0,0 +1,75 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<h3> | |||
project.info.notifications | |||
</h3> | |||
<Alert | |||
className="spacer-top" | |||
variant="info" | |||
> | |||
notification.dispatcher.information | |||
</Alert> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<table | |||
className="data zebra notifications-table" | |||
> | |||
<thead> | |||
<tr> | |||
<th | |||
aria-label="project" | |||
/> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
Array [ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"organization": "org", | |||
"project": "foo", | |||
"projectName": "Foo", | |||
"type": "type-global", | |||
}, | |||
] | |||
} | |||
onAdd={[Function]} | |||
onRemove={[Function]} | |||
project={true} | |||
types={ | |||
Array [ | |||
"type-common", | |||
] | |||
} | |||
/> | |||
</table> | |||
</DeferredSpinner> | |||
</Fragment> | |||
`; |
@@ -137,7 +137,7 @@ th.hide-overflow { | |||
} | |||
.big-padded { | |||
padding: calc(2 * var(--gridSize)); | |||
padding: calc(2 * var(--gridSize)) !important; | |||
} | |||
.padded-top { | |||
@@ -152,10 +152,6 @@ th.hide-overflow { | |||
padding-top: calc(var(--gridSize) / 2) !important; | |||
} | |||
.little-padded-bottom { | |||
padding-bottom: calc(var(--gridSize) / 2) !important; | |||
} | |||
td.little-spacer-left { | |||
padding-left: 4px !important; | |||
} | |||
@@ -210,6 +206,10 @@ th.huge-spacer-right { | |||
float: right !important; | |||
} | |||
.borderless { | |||
border: none !important; | |||
} | |||
.bordered { | |||
border: 1px solid var(--barBorderColor); | |||
} | |||
@@ -234,6 +234,10 @@ th.huge-spacer-right { | |||
overflow: hidden !important; | |||
} | |||
.overflow-y-auto { | |||
overflow-y: auto !important; | |||
} | |||
.max-width-100 { | |||
max-width: 100% !important; | |||
} | |||
@@ -301,6 +305,10 @@ th.huge-spacer-right { | |||
width: 600px !important; | |||
} | |||
.max-height-100 { | |||
max-height: 100% !important; | |||
} | |||
.justify { | |||
margin-bottom: -1em; | |||
text-align: justify; |
@@ -25,8 +25,8 @@ import Level from 'sonar-ui-common/components/ui/Level'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import MetaLink from '../../overview/meta/MetaLink'; | |||
import { orderLinks } from '../../projectLinks/utils'; | |||
import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink'; | |||
import { orderLinks } from '../../../helpers/projectLinks'; | |||
interface Props { | |||
project: T.MyProject; |
@@ -1,127 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import Modal from 'sonar-ui-common/components/controls/Modal'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import CodeSnippet from '../../../components/common/CodeSnippet'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import BadgeButton from './BadgeButton'; | |||
import BadgeParams from './BadgeParams'; | |||
import './styles.css'; | |||
import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
metrics: T.Dict<T.Metric>; | |||
project: string; | |||
qualifier: string; | |||
} | |||
interface State { | |||
open: boolean; | |||
selectedType: BadgeType; | |||
badgeOptions: BadgeOptions; | |||
} | |||
export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
state: State = { | |||
open: false, | |||
selectedType: BadgeType.measure, | |||
badgeOptions: { color: 'white', metric: 'alert_status' } | |||
}; | |||
handleClose = () => { | |||
this.setState({ open: false }); | |||
}; | |||
handleOpen = () => { | |||
this.setState({ open: true }); | |||
}; | |||
handleSelectBadge = (selectedType: BadgeType) => { | |||
this.setState({ selectedType }); | |||
}; | |||
handleUpdateOptions = (options: Partial<BadgeOptions>) => { | |||
this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } })); | |||
}; | |||
render() { | |||
const { branchLike, project, qualifier } = this.props; | |||
const { selectedType, badgeOptions } = this.state; | |||
const header = translate('overview.badges.title'); | |||
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) }; | |||
const badges = isSonarCloud() | |||
? [BadgeType.measure, BadgeType.qualityGate, BadgeType.marketing] | |||
: [BadgeType.measure, BadgeType.qualityGate]; | |||
return ( | |||
<div className="overview-meta-card"> | |||
<Button className="js-project-badges" onClick={this.handleOpen}> | |||
{translate('overview.badges.get_badge', qualifier)} | |||
</Button> | |||
{this.state.open && ( | |||
<Modal contentLabel={header} onRequestClose={this.handleClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
<p className="huge-spacer-bottom"> | |||
{translate('overview.badges.description', qualifier)} | |||
</p> | |||
<div className="badges-list spacer-bottom"> | |||
{badges.map(type => ( | |||
<BadgeButton | |||
key={type} | |||
onClick={this.handleSelectBadge} | |||
selected={type === selectedType} | |||
type={type} | |||
url={getBadgeUrl(type, fullBadgeOptions)} | |||
/> | |||
))} | |||
</div> | |||
<p className="text-center note huge-spacer-bottom"> | |||
{translate('overview.badges', selectedType, 'description', qualifier)} | |||
</p> | |||
<BadgeParams | |||
className="big-spacer-bottom" | |||
metrics={this.props.metrics} | |||
options={badgeOptions} | |||
type={selectedType} | |||
updateOptions={this.handleUpdateOptions} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={getBadgeSnippet(selectedType, fullBadgeOptions)} | |||
/> | |||
</div> | |||
<footer className="modal-foot"> | |||
<ResetButtonLink className="js-modal-close" onClick={this.handleClose}> | |||
{translate('close')} | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,180 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display the modal after click on sonarcloud 1`] = ` | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<Button | |||
className="js-project-badges" | |||
onClick={[Function]} | |||
> | |||
overview.badges.get_badge.TRK | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should display the modal after click on sonarcloud 2`] = ` | |||
<Modal | |||
contentLabel="overview.badges.title" | |||
onRequestClose={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
overview.badges.title | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<p | |||
className="huge-spacer-bottom" | |||
> | |||
overview.badges.description.TRK | |||
</p> | |||
<div | |||
className="badges-list spacer-bottom" | |||
> | |||
<BadgeButton | |||
key="measure" | |||
onClick={[Function]} | |||
selected={true} | |||
type="measure" | |||
url="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status" | |||
/> | |||
<BadgeButton | |||
key="quality_gate" | |||
onClick={[Function]} | |||
selected={false} | |||
type="quality_gate" | |||
url="host/api/project_badges/quality_gate?branch=branch-6.6&project=foo" | |||
/> | |||
<BadgeButton | |||
key="marketing" | |||
onClick={[Function]} | |||
selected={false} | |||
type="marketing" | |||
url="host/images/project_badges/sonarcloud-white.svg" | |||
/> | |||
</div> | |||
<p | |||
className="text-center note huge-spacer-bottom" | |||
> | |||
overview.badges.measure.description.TRK | |||
</p> | |||
<BadgeParams | |||
className="big-spacer-bottom" | |||
metrics={Object {}} | |||
options={ | |||
Object { | |||
"color": "white", | |||
"metric": "alert_status", | |||
} | |||
} | |||
type="measure" | |||
updateOptions={[Function]} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)" | |||
/> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<ResetButtonLink | |||
className="js-modal-close" | |||
onClick={[Function]} | |||
> | |||
close | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`should display the modal after click on sonarqube 1`] = ` | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<Button | |||
className="js-project-badges" | |||
onClick={[Function]} | |||
> | |||
overview.badges.get_badge.TRK | |||
</Button> | |||
</div> | |||
`; | |||
exports[`should display the modal after click on sonarqube 2`] = ` | |||
<Modal | |||
contentLabel="overview.badges.title" | |||
onRequestClose={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
overview.badges.title | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<p | |||
className="huge-spacer-bottom" | |||
> | |||
overview.badges.description.TRK | |||
</p> | |||
<div | |||
className="badges-list spacer-bottom" | |||
> | |||
<BadgeButton | |||
key="measure" | |||
onClick={[Function]} | |||
selected={true} | |||
type="measure" | |||
url="host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status" | |||
/> | |||
<BadgeButton | |||
key="quality_gate" | |||
onClick={[Function]} | |||
selected={false} | |||
type="quality_gate" | |||
url="host/api/project_badges/quality_gate?branch=branch-6.6&project=foo" | |||
/> | |||
</div> | |||
<p | |||
className="text-center note huge-spacer-bottom" | |||
> | |||
overview.badges.measure.description.TRK | |||
</p> | |||
<BadgeParams | |||
className="big-spacer-bottom" | |||
metrics={Object {}} | |||
options={ | |||
Object { | |||
"color": "white", | |||
"metric": "alert_status", | |||
} | |||
} | |||
type="measure" | |||
updateOptions={[Function]} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.6&project=foo&metric=alert_status)](/dashboard)" | |||
/> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<ResetButtonLink | |||
className="js-modal-close" | |||
onClick={[Function]} | |||
> | |||
close | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
`; |
@@ -1,171 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer'; | |||
import { hasPrivateAccess } from '../../../helpers/organizations'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { | |||
getAppState, | |||
getCurrentUser, | |||
getMyOrganizations, | |||
getOrganizationByKey, | |||
Store | |||
} from '../../../store/rootReducer'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import MetaKey from './MetaKey'; | |||
import MetaLinks from './MetaLinks'; | |||
import MetaOrganizationKey from './MetaOrganizationKey'; | |||
import MetaQualityGate from './MetaQualityGate'; | |||
import MetaQualityProfiles from './MetaQualityProfiles'; | |||
import MetaTags from './MetaTags'; | |||
const ProjectBadges = lazyLoadComponent(() => import('../badges/ProjectBadges'), 'ProjectBadges'); | |||
const ProjectNotifications = lazyLoadComponent( | |||
() => import('../notifications/ProjectNotifications'), | |||
'ProjectNotifications' | |||
); | |||
interface StateToProps { | |||
appState: T.AppState; | |||
currentUser: T.CurrentUser; | |||
organization?: T.Organization; | |||
userOrganizations: T.Organization[]; | |||
} | |||
interface OwnProps { | |||
branchLike?: BranchLike; | |||
component: T.Component; | |||
history?: { | |||
[metric: string]: Array<{ date: Date; value?: string }>; | |||
}; | |||
measures?: T.MeasureEnhanced[]; | |||
metrics?: T.Dict<T.Metric>; | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
type Props = OwnProps & StateToProps; | |||
export class Meta extends React.PureComponent<Props> { | |||
renderQualityInfos() { | |||
const { organizationsEnabled } = this.props.appState; | |||
const { component, currentUser, organization, userOrganizations } = this.props; | |||
const { qualifier, qualityProfiles, qualityGate } = component; | |||
const isProject = qualifier === 'TRK'; | |||
if ( | |||
!isProject || | |||
(organizationsEnabled && !hasPrivateAccess(currentUser, organization, userOrganizations)) | |||
) { | |||
return null; | |||
} | |||
return ( | |||
<div className="overview-meta-card" id="overview-meta-quality-gate"> | |||
{qualityGate && ( | |||
<MetaQualityGate | |||
organization={organizationsEnabled ? component.organization : undefined} | |||
qualityGate={qualityGate} | |||
/> | |||
)} | |||
{qualityProfiles && qualityProfiles.length > 0 && ( | |||
<MetaQualityProfiles | |||
headerClassName={qualityGate ? 'big-spacer-top' : undefined} | |||
organization={organizationsEnabled ? component.organization : undefined} | |||
profiles={qualityProfiles} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} | |||
render() { | |||
const { organizationsEnabled } = this.props.appState; | |||
const { branchLike, component, currentUser, metrics, organization } = this.props; | |||
const { qualifier, description, visibility } = component; | |||
const isProject = qualifier === 'TRK'; | |||
const isApp = qualifier === 'APP'; | |||
const isPrivate = visibility === 'private'; | |||
const canUseBadges = !isPrivate && (isProject || isApp); | |||
const canConfigureNotifications = isLoggedIn(currentUser); | |||
return ( | |||
<div className="overview-meta"> | |||
<div className="overview-meta-card"> | |||
<h4 className="overview-meta-header"> | |||
{translate('overview.about_this_project', qualifier)} | |||
{component.visibility && ( | |||
<PrivacyBadgeContainer | |||
className="spacer-left pull-right" | |||
organization={organization} | |||
qualifier={component.qualifier} | |||
tooltipProps={{ projectKey: component.key }} | |||
visibility={component.visibility} | |||
/> | |||
)} | |||
</h4> | |||
{description !== undefined && <p className="overview-meta-description">{description}</p>} | |||
{isProject && ( | |||
<MetaTags component={component} onComponentChange={this.props.onComponentChange} /> | |||
)} | |||
</div> | |||
{this.renderQualityInfos()} | |||
{isProject && <MetaLinks component={component} />} | |||
<div className="overview-meta-card"> | |||
<MetaKey componentKey={component.key} qualifier={component.qualifier} /> | |||
{organizationsEnabled && <MetaOrganizationKey organization={component.organization} />} | |||
</div> | |||
{(canUseBadges || canConfigureNotifications) && ( | |||
<div className="overview-meta-card"> | |||
{canUseBadges && metrics !== undefined && ( | |||
<ProjectBadges | |||
branchLike={branchLike} | |||
metrics={metrics} | |||
project={component.key} | |||
qualifier={component.qualifier} | |||
/> | |||
)} | |||
{canConfigureNotifications && ( | |||
<ProjectNotifications className="spacer-top spacer-bottom" component={component} /> | |||
)} | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: Store, { component }: OwnProps) => ({ | |||
appState: getAppState(state), | |||
currentUser: getCurrentUser(state), | |||
organization: getOrganizationByKey(state, component.organization), | |||
userOrganizations: getMyOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(Meta); |
@@ -1,109 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import SizeRating from 'sonar-ui-common/components/ui/SizeRating'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import LanguageDistributionContainer from '../../../components/charts/LanguageDistributionContainer'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: T.LightComponent; | |||
measures: T.MeasureEnhanced[]; | |||
} | |||
export default class MetaSize extends React.PureComponent<Props> { | |||
renderLoC = (ncloc?: T.MeasureEnhanced) => ( | |||
<div | |||
className={classNames('overview-meta-size-ncloc', { | |||
'is-half-width': this.props.component.qualifier === 'APP' | |||
})} | |||
id="overview-ncloc"> | |||
{ncloc && ( | |||
<span className="spacer-right"> | |||
<SizeRating value={Number(ncloc.value)} /> | |||
</span> | |||
)} | |||
{ncloc ? ( | |||
<DrilldownLink | |||
branchLike={this.props.branchLike} | |||
component={this.props.component.key} | |||
metric="ncloc"> | |||
{formatMeasure(ncloc.value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
) : ( | |||
<span>0</span> | |||
)} | |||
</div> | |||
); | |||
renderLoCDistribution = () => { | |||
const languageDistribution = this.props.measures.find( | |||
measure => measure.metric.key === 'ncloc_language_distribution' | |||
); | |||
const className = | |||
this.props.component.qualifier === 'TRK' ? 'overview-meta-size-lang-dist' : 'big-spacer-top'; | |||
return languageDistribution && languageDistribution.value !== undefined ? ( | |||
<div className={className} id="overview-language-distribution"> | |||
<LanguageDistributionContainer distribution={languageDistribution.value} width={160} /> | |||
</div> | |||
) : null; | |||
}; | |||
renderProjects = () => { | |||
const projects = this.props.measures.find(measure => measure.metric.key === 'projects'); | |||
return ( | |||
<div className="overview-meta-size-ncloc is-half-width" id="overview-projects"> | |||
{projects ? ( | |||
<DrilldownLink | |||
branchLike={this.props.branchLike} | |||
component={this.props.component.key} | |||
metric="projects"> | |||
{formatMeasure(projects.value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
) : ( | |||
<span>0</span> | |||
)} | |||
<div className="spacer-top text-muted">{translate('metric.projects.name')}</div> | |||
</div> | |||
); | |||
}; | |||
render() { | |||
const ncloc = this.props.measures.find(measure => measure.metric.key === 'ncloc'); | |||
if (ncloc == null && this.props.component.qualifier !== 'APP') { | |||
return null; | |||
} | |||
return ( | |||
<div className="big-spacer-top" id="overview-size"> | |||
{this.props.component.qualifier === 'APP' && this.renderProjects()} | |||
{this.renderLoC(ncloc)} | |||
{this.renderLoCDistribution()} | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,69 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 { | |||
mockAppState, | |||
mockComponent, | |||
mockLoggedInUser, | |||
mockOrganization | |||
} from '../../../../helpers/testMocks'; | |||
import { Meta } from '../MetaContainer'; | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(metaQualityGateRendered(wrapper)).toBe(true); | |||
}); | |||
it('should hide QG and QP links if the organization has a paid plan, and the user is not a member', () => { | |||
const wrapper = shallowRender({ | |||
organization: mockOrganization({ key: 'other_key', subscription: 'PAID' }) | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(metaQualityGateRendered(wrapper)).toBe(false); | |||
}); | |||
it('should show QG and QP links if the organization has a paid plan, and the user is a member', () => { | |||
const wrapper = shallowRender({ | |||
organization: mockOrganization({ subscription: 'PAID' }) | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(metaQualityGateRendered(wrapper)).toBe(true); | |||
}); | |||
function metaQualityGateRendered(wrapper: any) { | |||
return wrapper.find('#overview-meta-quality-gate').exists(); | |||
} | |||
function shallowRender(props: Partial<Meta['props']> = {}) { | |||
return shallow( | |||
<Meta | |||
appState={mockAppState({ organizationsEnabled: true })} | |||
component={mockComponent()} | |||
currentUser={mockLoggedInUser()} | |||
metrics={{}} | |||
onComponentChange={jest.fn()} | |||
organization={mockOrganization()} | |||
userOrganizations={[mockOrganization()]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,398 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should hide QG and QP links if the organization has a paid plan, and the user is not a member 1`] = ` | |||
<div | |||
className="overview-meta" | |||
> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<h4 | |||
className="overview-meta-header" | |||
> | |||
overview.about_this_project.TRK | |||
</h4> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
<MetaOrganizationKey | |||
organization="foo" | |||
/> | |||
</div> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<ProjectBadges | |||
metrics={Object {}} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
<ProjectNotifications | |||
className="spacer-top spacer-bottom" | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="overview-meta" | |||
> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<h4 | |||
className="overview-meta-header" | |||
> | |||
overview.about_this_project.TRK | |||
</h4> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="overview-meta-card" | |||
id="overview-meta-quality-gate" | |||
> | |||
<MetaQualityGate | |||
organization="foo" | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
organization="foo" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
<MetaOrganizationKey | |||
organization="foo" | |||
/> | |||
</div> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<ProjectBadges | |||
metrics={Object {}} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
<ProjectNotifications | |||
className="spacer-top spacer-bottom" | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should show QG and QP links if the organization has a paid plan, and the user is a member 1`] = ` | |||
<div | |||
className="overview-meta" | |||
> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<h4 | |||
className="overview-meta-header" | |||
> | |||
overview.about_this_project.TRK | |||
</h4> | |||
<MetaTags | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="overview-meta-card" | |||
id="overview-meta-quality-gate" | |||
> | |||
<MetaQualityGate | |||
organization="foo" | |||
qualityGate={ | |||
Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
} | |||
} | |||
/> | |||
<Connect(MetaQualityProfiles) | |||
headerClassName="big-spacer-top" | |||
organization="foo" | |||
profiles={ | |||
Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<MetaLinks | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<MetaKey | |||
componentKey="my-project" | |||
qualifier="TRK" | |||
/> | |||
<MetaOrganizationKey | |||
organization="foo" | |||
/> | |||
</div> | |||
<div | |||
className="overview-meta-card" | |||
> | |||
<ProjectBadges | |||
metrics={Object {}} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
<ProjectNotifications | |||
className="spacer-top spacer-bottom" | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"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 [], | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
`; |
@@ -1,116 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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 * as React from 'react'; | |||
import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import Modal from 'sonar-ui-common/components/controls/Modal'; | |||
import ModalButton from 'sonar-ui-common/components/controls/ModalButton'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
withNotifications, | |||
WithNotificationsProps | |||
} from '../../../components/hoc/withNotifications'; | |||
import NotificationsList from '../../account/notifications/NotificationsList'; | |||
interface Props { | |||
className?: string; | |||
component: T.Component; | |||
} | |||
export function ProjectNotifications(props: WithNotificationsProps & Props) { | |||
const { channels, className, component, loading, notifications, perProjectTypes } = props; | |||
const header = translate('my_account.notifications'); | |||
const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => { | |||
props.addNotification({ project: component.key, channel, type }); | |||
}; | |||
const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => { | |||
props.removeNotification({ | |||
project: component.key, | |||
channel, | |||
type | |||
}); | |||
}; | |||
const getCheckboxId = (type: string, channel: string) => { | |||
return `project-notification-${component.key}-${type}-${channel}`; | |||
}; | |||
const projectNotifications = notifications.filter(n => n.project && n.project === component.key); | |||
return ( | |||
<div className={className}> | |||
<ModalButton | |||
modal={({ onClose }) => ( | |||
<Modal contentLabel={header} onRequestClose={onClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
<Alert variant="info">{translate('notification.dispatcher.information')}</Alert> | |||
<DeferredSpinner loading={loading}> | |||
<table className="data zebra notifications-table"> | |||
<thead> | |||
<tr> | |||
<th aria-label={translate('project')} /> | |||
{channels.map(channel => ( | |||
<th className="text-center" key={channel}> | |||
<h4>{translate('notification.channel', channel)}</h4> | |||
</th> | |||
))} | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={channels} | |||
checkboxId={getCheckboxId} | |||
notifications={projectNotifications} | |||
onAdd={handleAddNotification} | |||
onRemove={handleRemoveNotification} | |||
project={true} | |||
types={perProjectTypes} | |||
/> | |||
</table> | |||
</DeferredSpinner> | |||
</div> | |||
<footer className="modal-foot"> | |||
<ResetButtonLink className="js-modal-close" onClick={onClose}> | |||
{translate('close')} | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
)}> | |||
{({ onClick }) => ( | |||
<Button onClick={onClick}> | |||
<span data-test="overview__edit-notifications"> | |||
{translate('my_profile.per_project_notifications.edit')} | |||
</span> | |||
</Button> | |||
)} | |||
</ModalButton> | |||
</div> | |||
); | |||
} | |||
export default withNotifications(ProjectNotifications); |
@@ -1,108 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="my_account.notifications" | |||
isOpen={true} | |||
onRequestClose={[MockFunction]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
role="dialog" | |||
shouldCloseOnEsc={true} | |||
shouldCloseOnOverlayClick={true} | |||
shouldFocusAfterRender={true} | |||
shouldReturnFocusAfterClose={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
my_account.notifications | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<Alert | |||
variant="info" | |||
> | |||
notification.dispatcher.information | |||
</Alert> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<table | |||
className="data zebra notifications-table" | |||
> | |||
<thead> | |||
<tr> | |||
<th | |||
aria-label="project" | |||
/> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
Array [ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"organization": "org", | |||
"project": "foo", | |||
"projectName": "Foo", | |||
"type": "type-global", | |||
}, | |||
] | |||
} | |||
onAdd={[Function]} | |||
onRemove={[Function]} | |||
project={true} | |||
types={ | |||
Array [ | |||
"type-common", | |||
] | |||
} | |||
/> | |||
</table> | |||
</DeferredSpinner> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<ResetButtonLink | |||
className="js-modal-close" | |||
onClick={[MockFunction]} | |||
> | |||
close | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
`; |
@@ -168,73 +168,6 @@ | |||
vertical-align: top; | |||
} | |||
/* | |||
* Meta | |||
TODO REMOVE ME!! | |||
*/ | |||
.overview-meta { | |||
background-color: var(--barBackgroundColor); | |||
} | |||
.overview-meta-card { | |||
min-width: 200px; | |||
box-sizing: border-box; | |||
} | |||
.overview-meta-card + .overview-meta-card { | |||
margin-top: calc(2 * var(--gridSize)); | |||
padding-top: calc(2 * var(--gridSize) - 1px); | |||
border-top: 1px solid var(--barBorderColor); | |||
} | |||
.overview-meta-description { | |||
margin-top: calc(-0.5 * var(--gridSize)); | |||
line-height: 1.5; | |||
color: var(--secondFontColor); | |||
} | |||
.overview-meta-header { | |||
margin-bottom: calc(0.5 * var(--gridSize)); | |||
color: var(--baseFontColor); | |||
} | |||
.overview-meta-list > li { | |||
/* 1px to not cut icons on the left */ | |||
padding-left: 1px; | |||
padding-bottom: 4px; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
} | |||
.overview-meta-tags { | |||
position: relative; | |||
} | |||
.overview-meta-size-lang-dist { | |||
display: inline-block; | |||
vertical-align: middle; | |||
width: 160px; | |||
min-height: 40px; | |||
border-left: 1px solid var(--barBorderColor); | |||
box-sizing: border-box; | |||
} | |||
.overview-key { | |||
width: 100%; | |||
background-color: transparent !important; | |||
} | |||
.overview-deleted-profile, | |||
.overview-deprecated-rules { | |||
margin: 4px -6px 4px; | |||
padding: 3px 6px !important; | |||
border: 1px solid var(--alertBorderError); | |||
border-radius: 3px; | |||
background-color: var(--alertBackgroundError); | |||
} | |||
/* | |||
* Animations | |||
*/ |
@@ -23,7 +23,7 @@ import ConfirmButton from 'sonar-ui-common/components/controls/ConfirmButton'; | |||
import ProjectLinkIcon from 'sonar-ui-common/components/icons/ProjectLinkIcon'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import isValidUri from '../../app/utils/isValidUri'; | |||
import { getLinkName, isProvided } from './utils'; | |||
import { getLinkName, isProvided } from '../../helpers/projectLinks'; | |||
interface Props { | |||
link: T.ProjectLink; |
@@ -19,8 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { orderLinks } from '../../helpers/projectLinks'; | |||
import LinkRow from './LinkRow'; | |||
import { orderLinks } from './utils'; | |||
interface Props { | |||
links: T.ProjectLink[]; |
@@ -17,11 +17,11 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as utils from '../utils'; | |||
import { getLinkName, isProvided, orderLinks } from '../projectLinks'; | |||
it('#isProvided', () => { | |||
expect(utils.isProvided({ type: 'homepage' })).toBe(true); | |||
expect(utils.isProvided({ type: 'custom' })).toBe(false); | |||
expect(isProvided({ type: 'homepage' })).toBe(true); | |||
expect(isProvided({ type: 'custom' })).toBe(false); | |||
}); | |||
it('#orderLinks', () => { | |||
@@ -29,12 +29,12 @@ it('#orderLinks', () => { | |||
const issues = { type: 'issue' }; | |||
const foo = { name: 'foo', type: 'foo' }; | |||
const bar = { name: 'bar', type: 'bar' }; | |||
expect(utils.orderLinks([foo, homepage, issues, bar])).toEqual([homepage, issues, bar, foo]); | |||
expect(utils.orderLinks([foo, bar])).toEqual([bar, foo]); | |||
expect(utils.orderLinks([issues, homepage])).toEqual([homepage, issues]); | |||
expect(orderLinks([foo, homepage, issues, bar])).toEqual([homepage, issues, bar, foo]); | |||
expect(orderLinks([foo, bar])).toEqual([bar, foo]); | |||
expect(orderLinks([issues, homepage])).toEqual([homepage, issues]); | |||
}); | |||
it('#getLinkName', () => { | |||
expect(utils.getLinkName({ type: 'homepage' })).toBe('project_links.homepage'); | |||
expect(utils.getLinkName({ name: 'foo', type: 'custom' })).toBe('foo'); | |||
expect(getLinkName({ type: 'homepage' })).toBe('project_links.homepage'); | |||
expect(getLinkName({ name: 'foo', type: 'custom' })).toBe('foo'); | |||
}); |
@@ -1318,7 +1318,7 @@ project_quality_profile.successfully_updated={0} quality profile has been succes | |||
# | |||
#------------------------------------------------------------------------------ | |||
project_quality_gate.default_qgate=Default | |||
project_quality_gate.successfully_updated=Quality gate has been successfully updated. | |||
project_quality_gate.successfully_updated=Quality Gate has been successfully updated. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -1333,6 +1333,18 @@ projects_management.delete_selected_warning=You're about to delete {0} selected | |||
projects_management.delete_all_warning=You're about to delete all {0} items. | |||
projects_management.project_has_been_successfully_created=Project {project} has been successfully created. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# PROJECT INFORMATION DRAWER | |||
# | |||
#------------------------------------------------------------------------------ | |||
project.info.title=Project information | |||
application.info.title=Application information | |||
project.info.description=Description | |||
project.info.quality_gate=Quality Gate used | |||
project.info.to_notifications=Set notifications | |||
project.info.notifications=Set notifications | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -2654,7 +2666,7 @@ overview.you_should_define_quality_gate=You should define a quality gate on this | |||
overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines | |||
overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. | |||
overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details. | |||
overview.quality_profiles=Quality Profiles | |||
overview.quality_profiles=Quality Profiles used | |||
overview.new_code_period_x=New Code: {0} | |||
overview.max_new_code_period_from_x=Max New Code from: {0} | |||
overview.started_x=Started {0} | |||
@@ -2724,11 +2736,10 @@ overview.complexity_tooltip.file={0} files have complexity around {1} | |||
overview.deprecated_profile=This quality profile uses {0} deprecated rules and should be updated. | |||
overview.deleted_profile={0} has been deleted since the last analysis. | |||
overview.badges.get_badge.TRK=Get project badges | |||
overview.badges.get_badge.VW=Get portfolio badges | |||
overview.badges.get_badge.APP=Get application badges | |||
overview.badges.title=Badges | |||
overview.badges.title=Get project badges | |||
overview.badges.description.TRK=Show the status of your project metrics on your README or website. Pick your style: | |||
overview.badges.description.VW=Show the status of your portfolio metrics on your README or website. Pick your style: | |||
overview.badges.description.APP=Show the status of your application metrics on your README or website. Pick your style: | |||
@@ -2739,17 +2750,17 @@ overview.badges.options.colors.orange=Orange | |||
overview.badges.options.formats.md=Markdown | |||
overview.badges.options.formats.url=Image URL only | |||
overview.badges.measure.alt=Standard badge | |||
overview.badges.measure.description.TRK=This badge dynamically displays the current status of one metric of your project. | |||
overview.badges.measure.description.VW=This badge dynamically displays the current status of one metric of your portfolio. | |||
overview.badges.measure.description.APP=This badge dynamically displays the current status of one metric of your application. | |||
overview.badges.measure.description.TRK=Displays the current status of one metric of your project. | |||
overview.badges.measure.description.VW=Displays the current status of one metric of your portfolio. | |||
overview.badges.measure.description.APP=Displays the current status of one metric of your application. | |||
overview.badges.marketing.alt=Scanned on SonarCloud badge | |||
overview.badges.marketing.description=This badge lets you advertise that you're using SonarCloud for code quality. | |||
overview.badges.marketing.description.TRK=This badge lets you advertise that you're using SonarCloud for code quality. | |||
overview.badges.quality_gate.alt=Quality Gate badge | |||
overview.badges.quality_gate.description=This badge dynamically displays the current quality gate status of your project. | |||
overview.badges.quality_gate.description.APP=This badge dynamically displays the current quality gate status of your application. | |||
overview.badges.quality_gate.description.TRK=This badge dynamically displays the current quality gate status of your project. | |||
overview.badges.quality_gate.description.VW=This badge dynamically displays the current quality gate status of your portfolio. | |||
overview.badges.quality_gate.description=Displays the current quality gate status of your project. | |||
overview.badges.quality_gate.description.APP=Displays the current quality gate status of your application. | |||
overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project. | |||
overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio. | |||
#------------------------------------------------------------------------------ |