@@ -107,20 +107,26 @@ export function getTree(component: string, options?: Object = {}) { | |||
return getJSON(url, data); | |||
} | |||
export function getParents({ id, key }: { id: string, key: string }) { | |||
export function getComponentShow(component: string) { | |||
const url = '/api/components/show'; | |||
const data = id ? { id } : { key }; | |||
return getJSON(url, data).then(r => r.ancestors); | |||
return getJSON(url, { component }); | |||
} | |||
export function getParents(component: string) { | |||
return getComponentShow(component).then(r => r.ancestors); | |||
} | |||
export function getBreadcrumbs(component: string) { | |||
const url = '/api/components/show'; | |||
return getJSON(url, { component }).then(r => { | |||
return getComponentShow(component).then(r => { | |||
const reversedAncestors = [...r.ancestors].reverse(); | |||
return [...reversedAncestors, r.component]; | |||
}); | |||
} | |||
export function getComponentTags(component: string) { | |||
return getComponentShow(component).then(r => r.component.tags || []); | |||
} | |||
export function getMyProjects(data?: Object) { | |||
const url = '/api/projects/search_my_projects'; | |||
return getJSON(url, data); |
@@ -29,7 +29,8 @@ type Props = { | |||
component: { | |||
id: string, | |||
key: string, | |||
qualifier: string | |||
qualifier: string, | |||
tags: Array<string> | |||
}, | |||
router: Object | |||
}; |
@@ -26,7 +26,9 @@ import MetaQualityGate from './MetaQualityGate'; | |||
import MetaQualityProfiles from './MetaQualityProfiles'; | |||
import AnalysesList from '../events/AnalysesList'; | |||
import MetaSize from './MetaSize'; | |||
import TagsList from '../../../components/ui/TagsList'; | |||
import { areThereCustomOrganizations } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const Meta = ({ component, measures, areThereCustomOrganizations }) => { | |||
const { qualifier, description, qualityProfiles, qualityGate } = component; | |||
@@ -41,9 +43,10 @@ const Meta = ({ component, measures, areThereCustomOrganizations }) => { | |||
const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles; | |||
const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate; | |||
const shouldShowOrganizationKey = component.organization != null && areThereCustomOrganizations; | |||
const configuration = component.configuration || {}; | |||
return ( | |||
<div className="overview-meta"> | |||
{hasDescription && | |||
@@ -53,6 +56,14 @@ const Meta = ({ component, measures, areThereCustomOrganizations }) => { | |||
<MetaSize component={component} measures={measures} /> | |||
<div className="overview-meta-card"> | |||
<TagsList | |||
tags={component.tags.length ? component.tags : [translate('no_tags')]} | |||
allowUpdate={configuration.showSettings} | |||
allowMultiLine={true} | |||
/> | |||
</div> | |||
{shouldShowQualityGate && <MetaQualityGate gate={qualityGate} />} | |||
{shouldShowQualityProfiles && <MetaQualityProfiles profiles={qualityProfiles} />} |
@@ -26,6 +26,7 @@ import ProjectCardQualityGate from './ProjectCardQualityGate'; | |||
import ProjectCardMeasures from './ProjectCardMeasures'; | |||
import FavoriteContainer from '../../../components/controls/FavoriteContainer'; | |||
import Organization from '../../../components/shared/Organization'; | |||
import TagsList from '../../../components/ui/TagsList'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default class ProjectCard extends React.PureComponent { | |||
@@ -35,7 +36,10 @@ export default class ProjectCard extends React.PureComponent { | |||
project?: { | |||
analysisDate?: string, | |||
key: string, | |||
name: string | |||
name: string, | |||
tags: Array<string>, | |||
isFavorite?: boolean, | |||
organization?: string | |||
} | |||
}; | |||
@@ -74,6 +78,7 @@ export default class ProjectCard extends React.PureComponent { | |||
{project.name} | |||
</Link> | |||
</h2> | |||
{project.tags.length > 0 && <TagsList tags={project.tags} />} | |||
</div> | |||
{isProjectAnalyzed |
@@ -21,7 +21,7 @@ import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCard from '../ProjectCard'; | |||
const PROJECT = { analysisDate: '2017-01-01', key: 'foo', name: 'Foo' }; | |||
const PROJECT = { analysisDate: '2017-01-01', key: 'foo', name: 'Foo', tags: [] }; | |||
const MEASURES = {}; | |||
it('should display analysis date', () => { | |||
@@ -44,3 +44,8 @@ it('should NOT display analysis date', () => { | |||
it('should display loading', () => { | |||
expect(shallow(<ProjectCard project={PROJECT} />)).toMatchSnapshot(); | |||
}); | |||
it('should display tags', () => { | |||
const project = { ...PROJECT, tags: ['foo', 'bar'] }; | |||
expect(shallow(<ProjectCard project={project} />)).toMatchSnapshot(); | |||
}); |
@@ -36,3 +36,44 @@ exports[`test should display loading 1`] = ` | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should display tags 1`] = ` | |||
<div | |||
className="boxed-group project-card boxed-group-loading" | |||
data-key="foo"> | |||
<div | |||
className="boxed-group-header"> | |||
<h2 | |||
className="project-card-name"> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
}> | |||
Foo | |||
</Link> | |||
</h2> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={false} | |||
tags={ | |||
Array [ | |||
"foo", | |||
"bar", | |||
] | |||
} /> | |||
</div> | |||
<div | |||
className="boxed-group-inner" /> | |||
<div | |||
className="project-card-analysis-date note"> | |||
overview.last_analysis_on_x.January 1, 2017 12:00 AM | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,20 @@ | |||
.tags-list { | |||
padding-left: 6px; | |||
} | |||
.tags-list i { | |||
padding-left: 4px; | |||
} | |||
.tags-list i::before { | |||
font-size: 12px; | |||
} | |||
.tags-list span { | |||
display: inline-block; | |||
vertical-align: text-top; | |||
max-width: 220px; | |||
padding-left: 4px; | |||
margin-top: 2px; | |||
opacity: 0.6; | |||
} |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import './TagsList.css'; | |||
type Props = { | |||
tags: Array<string>, | |||
allowUpdate: boolean, | |||
allowMultiLine: boolean | |||
}; | |||
export default class TagsList extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
allowUpdate: false, | |||
allowMultiLine: false | |||
}; | |||
render() { | |||
const { tags, allowUpdate } = this.props; | |||
const spanClass = classNames('note', { | |||
'text-ellipsis': !this.props.allowMultiLine | |||
}); | |||
return ( | |||
<span className="tags-list" title={tags.join(', ')}> | |||
<i className="icon-tags icon-half-transparent" /> | |||
<span className={spanClass}>{tags.join(', ')}</span> | |||
{allowUpdate && <i className="icon-dropdown icon-half-transparent" />} | |||
</span> | |||
); | |||
} | |||
} |
@@ -0,0 +1,50 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 React from 'react'; | |||
import TagsList from '../TagsList'; | |||
const tags = ['foo', 'bar']; | |||
it('should render with a list of tag', () => { | |||
const taglist = shallow(<TagsList tags={tags} />); | |||
expect(taglist.text()).toBe(tags.join(', ')); | |||
expect(taglist.find('i').length).toBe(1); | |||
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(true); | |||
}); | |||
it('should FAIL to render without tags', () => { | |||
expect(() => shallow(<TagsList />)).toThrow(); | |||
}); | |||
it('should correctly handle a lot of tags', () => { | |||
const lotOfTags = []; | |||
for (let i = 0; i < 20; i++) { | |||
lotOfTags.push(tags); | |||
} | |||
const taglist = shallow(<TagsList tags={lotOfTags} allowMultiLine={true} />); | |||
expect(taglist.text()).toBe(lotOfTags.join(', ')); | |||
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(false); | |||
}); | |||
it('should render with a caret on the right if update is allowed', () => { | |||
const taglist = shallow(<TagsList tags={tags} allowUpdate={true} />); | |||
expect(taglist.find('i').length).toBe(2); | |||
}); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import { getLanguages } from '../api/languages'; | |||
import { getGlobalNavigation, getComponentNavigation } from '../api/nav'; | |||
import { getComponentTags } from '../api/components'; | |||
import * as auth from '../api/auth'; | |||
import { getOrganizations } from '../api/organizations'; | |||
import { receiveLanguages } from './languages/actions'; | |||
@@ -57,7 +58,8 @@ const addQualifier = project => ({ | |||
export const fetchProject = key => | |||
dispatch => | |||
getComponentNavigation(key).then(component => { | |||
Promise.all([getComponentNavigation(key), getComponentTags(key)]).then(([component, tags]) => { | |||
component.tags = tags; | |||
dispatch(receiveComponents([addQualifier(component)])); | |||
if (component.organization != null) { | |||
dispatch(fetchOrganizations([component.organization])); |
@@ -109,6 +109,7 @@ name_too_long_x=Name is too long (maximum is {0} characters) | |||
navigation=Navigation | |||
never=Never | |||
none=None | |||
no_tags=No tags | |||
off=Off | |||
on=On | |||
organization_key=Organization Key |