diff options
author | Grégoire Aubert <gregaubert@users.noreply.github.com> | 2017-03-20 13:15:55 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-20 13:15:55 +0100 |
commit | fff745c5b6f9e713a66dd9393ceafd9aad397f0d (patch) | |
tree | 837fc437a3781e7b1d0b31dffeec3add493083e4 /server/sonar-web/src/main/js | |
parent | ab8377a211348bc4ca17c731a71f6cd80f855eca (diff) | |
download | sonarqube-fff745c5b6f9e713a66dd9393ceafd9aad397f0d.tar.gz sonarqube-fff745c5b6f9e713a66dd9393ceafd9aad397f0d.zip |
SONAR-8843 Add the tags on the projects page and project homepage (#1801)
Diffstat (limited to 'server/sonar-web/src/main/js')
10 files changed, 204 insertions, 10 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index c2ec0a454f5..75fa13cb001 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 83a1f73c1e3..517569d47f9 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -29,7 +29,8 @@ type Props = { component: { id: string, key: string, - qualifier: string + qualifier: string, + tags: Array<string> }, router: Object }; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index b5102e26d64..c5dfaa64ecb 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -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} />} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js index 18efa9f2e64..6f3691c943d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.js index 23d90396a25..3961e537d86 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.js +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap index 43c9b8fa589..b53a20d538b 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/ui/TagsList.css b/server/sonar-web/src/main/js/components/ui/TagsList.css new file mode 100644 index 00000000000..bb48ef2bf21 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/TagsList.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/components/ui/TagsList.js b/server/sonar-web/src/main/js/components/ui/TagsList.js new file mode 100644 index 00000000000..6f568df0357 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/TagsList.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/TagsList-test.js b/server/sonar-web/src/main/js/components/ui/__tests__/TagsList-test.js new file mode 100644 index 00000000000..9eec4be8ed2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/TagsList-test.js @@ -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); +}); diff --git a/server/sonar-web/src/main/js/store/rootActions.js b/server/sonar-web/src/main/js/store/rootActions.js index 0e95f02d56c..58685e5cb27 100644 --- a/server/sonar-web/src/main/js/store/rootActions.js +++ b/server/sonar-web/src/main/js/store/rootActions.js @@ -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])); |