diff options
Diffstat (limited to 'server/sonar-web/src/main')
25 files changed, 929 insertions, 1114 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 684308a8dbc..df296aa6181 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -20,6 +20,7 @@ import { getJSON, post, postJSON, RequestData } from 'sonar-ui-common/helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; import { BranchParameters } from '../types/branch-like'; +import { ComponentQualifier } from '../types/component'; export interface BaseSearchProjectsParameters { analyzedBefore?: string; @@ -207,6 +208,7 @@ export interface Component { name: string; isFavorite?: boolean; analysisDate?: string; + qualifier: ComponentQualifier; tags: string[]; visibility: T.Visibility; leakPeriodDate?: string; diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index d6ec455fff5..83b3148fd36 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -144,10 +144,18 @@ th.hide-overflow { padding-top: var(--gridSize) !important; } +.padded-right { + padding-right: var(--gridSize) !important; +} + .padded-bottom { padding-bottom: var(--gridSize) !important; } +.padded-left { + padding-left: var(--gridSize) !important; +} + .little-padded-top { padding-top: calc(var(--gridSize) / 2) !important; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx index f2494aa86ac..bf49fab53fe 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx @@ -17,10 +17,25 @@ * 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 difference from 'date-fns/difference_in_milliseconds'; import * as React from 'react'; +import { Link } from 'react-router'; +import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer'; +import Favorite from '../../../components/controls/Favorite'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import TagsList from '../../../components/tags/TagsList'; +import { getProjectUrl } from '../../../helpers/urls'; +import { isLoggedIn } from '../../../helpers/users'; +import { ComponentQualifier } from '../../../types/component'; import { Project } from '../types'; -import ProjectCardLeak from './ProjectCardLeak'; -import ProjectCardOverall from './ProjectCardOverall'; +import { formatDuration } from '../utils'; +import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; +import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer'; +import ProjectCardOverallMeasures from './ProjectCardOverallMeasures'; +import ProjectCardQualityGate from './ProjectCardQualityGate'; interface Props { currentUser: T.CurrentUser; @@ -31,11 +46,150 @@ interface Props { type?: string; } -export default class ProjectCard extends React.PureComponent<Props> { - render() { - if (this.props.type === 'leak') { - return <ProjectCardLeak {...this.props} />; - } - return <ProjectCardOverall {...this.props} />; +interface Dates { + analysisDate: string; + leakPeriodDate?: string; +} + +function getDates(project: Project, type: string | undefined) { + const { analysisDate, leakPeriodDate } = project; + if (!analysisDate || (type === 'leak' && !leakPeriodDate)) { + return undefined; + } else { + return { analysisDate, leakPeriodDate }; } } + +function renderHeader(props: Props) { + const { organization, project } = props; + const hasTags = project.tags.length > 0; + return ( + <div className="project-card-header"> + {project.isFavorite !== undefined && ( + <Favorite + className="spacer-right" + component={project.key} + favorite={project.isFavorite} + handleFavorite={props.handleFavorite} + qualifier={project.qualifier} + /> + )} + <h2 className="project-card-name"> + {!organization && <ProjectCardOrganizationContainer organization={project.organization} />} + <Link to={getProjectUrl(project.key)}>{project.name}</Link> + </h2> + {project.analysisDate && <ProjectCardQualityGate status={project.measures['alert_status']} />} + <div className="project-card-header-right"> + <PrivacyBadgeContainer + className="spacer-left" + organization={organization || project.organization} + qualifier={project.qualifier} + tooltipProps={{ projectKey: project.key }} + visibility={project.visibility} + /> + + {hasTags && <TagsList className="spacer-left note" tags={project.tags} />} + </div> + </div> + ); +} + +function renderDates(dates: Dates, type: string | undefined) { + const { analysisDate, leakPeriodDate } = dates; + const periodMs = leakPeriodDate ? difference(Date.now(), leakPeriodDate) : 0; + + return ( + <> + <DateTimeFormatter date={analysisDate}> + {formattedDate => ( + <span className="note"> + {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + </span> + )} + </DateTimeFormatter> + {type === 'leak' && periodMs !== undefined && ( + <span className="project-card-leak-date big-spacer-left big-spacer-right"> + {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))} + </span> + )} + </> + ); +} + +function renderDateRow(project: Project, dates: Dates | undefined, type: string | undefined) { + if (project.qualifier === ComponentQualifier.Application || dates) { + return ( + <div + className={classNames('display-flex-center project-card-dates spacer-top', { + 'big-spacer-left padded-left': project.isFavorite !== undefined + })}> + {dates && renderDates(dates, type)} + + {project.qualifier === ComponentQualifier.Application && ( + <div className="text-right flex-1-0-auto"> + <QualifierIcon className="spacer-right" qualifier={project.qualifier} /> + {translate('qualifier.APP')} + {project.measures.projects && ( + <> + {' ‒ '} + {translateWithParameters('x_projects_', project.measures.projects)} + </> + )} + </div> + )} + </div> + ); + } else { + return null; + } +} + +function renderMeasures(props: Props, dates: Dates | undefined) { + const { currentUser, project, type } = props; + + const { measures } = project; + + if (dates) { + return type === 'leak' ? ( + <ProjectCardLeakMeasures measures={measures} /> + ) : ( + <ProjectCardOverallMeasures componentQualifier={project.qualifier} measures={measures} /> + ); + } else { + return ( + <div className="project-card-not-analyzed"> + <span className="note"> + {type === 'leak' && project.analysisDate + ? translate('projects.no_new_code_period', project.qualifier) + : translate('projects.not_analyzed', project.qualifier)} + </span> + {project.qualifier !== ComponentQualifier.Application && + !project.analysisDate && + isLoggedIn(currentUser) && ( + <Link className="button spacer-left" to={getProjectUrl(project.key)}> + {translate('projects.configure_analysis')} + </Link> + )} + </div> + ); + } +} + +export default function ProjectCard(props: Props) { + const { height, project, type } = props; + + const dates = getDates(project, type); + + return ( + <div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key={project.key} + style={{ height }}> + <div> + {renderHeader(props)} + {renderDateRow(project, dates, type)} + </div> + {renderMeasures(props, dates)} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx deleted file mode 100644 index cfec74b9366..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ /dev/null @@ -1,124 +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 difference from 'date-fns/difference_in_milliseconds'; -import * as React from 'react'; -import { Link } from 'react-router'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer'; -import Favorite from '../../../components/controls/Favorite'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import TagsList from '../../../components/tags/TagsList'; -import { getProjectUrl } from '../../../helpers/urls'; -import { isLoggedIn } from '../../../helpers/users'; -import { Project } from '../types'; -import { formatDuration } from '../utils'; -import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; -import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer'; -import ProjectCardQualityGate from './ProjectCardQualityGate'; - -interface Props { - currentUser: T.CurrentUser; - handleFavorite: (component: string, isFavorite: boolean) => void; - height: number; - organization: T.Organization | undefined; - project: Project; -} - -export default class ProjectCardLeak extends React.PureComponent<Props> { - render() { - const { currentUser, handleFavorite, height, organization, project } = this.props; - const { measures } = project; - const hasTags = project.tags.length > 0; - const periodMs = project.leakPeriodDate ? difference(Date.now(), project.leakPeriodDate) : 0; - - return ( - <div className="boxed-group project-card" data-key={project.key} style={{ height }}> - <div className="boxed-group-header clearfix"> - <div className="project-card-header"> - {project.isFavorite != null && ( - <Favorite - className="spacer-right" - component={project.key} - favorite={project.isFavorite} - handleFavorite={handleFavorite} - qualifier="TRK" - /> - )} - <h2 className="project-card-name"> - {!organization && ( - <ProjectCardOrganizationContainer organization={project.organization} /> - )} - <Link to={{ pathname: '/dashboard', query: { id: project.key } }}> - {project.name} - </Link> - </h2> - {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />} - <div className="project-card-header-right"> - <PrivacyBadgeContainer - className="spacer-left" - organization={organization || project.organization} - qualifier="TRK" - tooltipProps={{ projectKey: project.key }} - visibility={project.visibility} - /> - - {hasTags && <TagsList className="spacer-left note" tags={project.tags} />} - </div> - </div> - {project.analysisDate && project.leakPeriodDate && ( - <div className="project-card-dates note text-right pull-right"> - <span className="project-card-leak-date pull-right"> - {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))} - </span> - <DateTimeFormatter date={project.analysisDate}> - {formattedDate => ( - <span> - {translateWithParameters('projects.last_analysis_on_x', formattedDate)} - </span> - )} - </DateTimeFormatter> - </div> - )} - </div> - - {project.analysisDate && project.leakPeriodDate ? ( - <div className="boxed-group-inner"> - <ProjectCardLeakMeasures measures={measures} /> - </div> - ) : ( - <div className="boxed-group-inner"> - <div className="project-card-not-analyzed"> - <span className="note"> - {project.analysisDate - ? translate('projects.no_new_code_period') - : translate('projects.not_analyzed')} - </span> - {!project.analysisDate && isLoggedIn(currentUser) && ( - <Link className="button spacer-left" to={getProjectUrl(project.key)}> - {translate('projects.configure_analysis')} - </Link> - )} - </div> - </div> - )} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx deleted file mode 100644 index f918839d035..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ /dev/null @@ -1,112 +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 { Link } from 'react-router'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer'; -import Favorite from '../../../components/controls/Favorite'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import TagsList from '../../../components/tags/TagsList'; -import { getProjectUrl } from '../../../helpers/urls'; -import { isLoggedIn } from '../../../helpers/users'; -import { Project } from '../types'; -import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer'; -import ProjectCardOverallMeasures from './ProjectCardOverallMeasures'; -import ProjectCardQualityGate from './ProjectCardQualityGate'; - -interface Props { - currentUser: T.CurrentUser; - handleFavorite: (component: string, isFavorite: boolean) => void; - height: number; - organization: T.Organization | undefined; - project: Project; -} - -export default class ProjectCardOverall extends React.PureComponent<Props> { - render() { - const { currentUser, handleFavorite, height, organization, project } = this.props; - const { measures } = project; - - const hasTags = project.tags.length > 0; - - return ( - <div className="boxed-group project-card" data-key={project.key} style={{ height }}> - <div className="boxed-group-header clearfix"> - <div className="project-card-header"> - {project.isFavorite !== undefined && ( - <Favorite - className="spacer-right" - component={project.key} - favorite={project.isFavorite} - handleFavorite={handleFavorite} - qualifier="TRK" - /> - )} - <h2 className="project-card-name"> - {!organization && ( - <ProjectCardOrganizationContainer organization={project.organization} /> - )} - <Link to={getProjectUrl(project.key)}>{project.name}</Link> - </h2> - {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />} - <div className="project-card-header-right"> - <PrivacyBadgeContainer - className="spacer-left" - organization={organization || project.organization} - qualifier="TRK" - tooltipProps={{ projectKey: project.key }} - visibility={project.visibility} - /> - {hasTags && <TagsList className="spacer-left note" tags={project.tags} />} - </div> - </div> - {project.analysisDate && ( - <div className="project-card-dates note text-right"> - <DateTimeFormatter date={project.analysisDate}> - {formattedDate => ( - <span className="big-spacer-left"> - {translateWithParameters('projects.last_analysis_on_x', formattedDate)} - </span> - )} - </DateTimeFormatter> - </div> - )} - </div> - - {project.analysisDate ? ( - <div className="boxed-group-inner"> - {<ProjectCardOverallMeasures measures={measures} />} - </div> - ) : ( - <div className="boxed-group-inner"> - <div className="project-card-not-analyzed"> - <span className="note">{translate('projects.not_analyzed')}</span> - {isLoggedIn(currentUser) && ( - <Link className="button spacer-left" to={getProjectUrl(project.key)}> - {translate('projects.configure_analysis')} - </Link> - )} - </div> - </div> - )} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx index 9198fd8f42c..93ee98af1e6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx @@ -23,21 +23,29 @@ import SizeRating from 'sonar-ui-common/components/ui/SizeRating'; import { translate } from 'sonar-ui-common/helpers/l10n'; import Measure from '../../../components/measure/Measure'; import CoverageRating from '../../../components/ui/CoverageRating'; +import { ComponentQualifier } from '../../../types/component'; import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer'; import ProjectCardRatingMeasure from './ProjectCardRatingMeasure'; interface Props { + componentQualifier: ComponentQualifier; measures: T.Dict<string | undefined>; } -export default function ProjectCardOverallMeasures({ measures }: Props) { +export default function ProjectCardOverallMeasures({ componentQualifier, measures }: Props) { if (measures === undefined) { return null; } const { ncloc } = measures; if (!ncloc) { - return <div className="note">{translate('overview.project.main_branch_empty')}</div>; + return ( + <div className="note big-spacer-top"> + {componentQualifier === ComponentQualifier.Application + ? translate('portfolio.app.empty') + : translate('overview.project.main_branch_empty')} + </div> + ); } return ( diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 794c7afc07f..3add3f6bbcb 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { get, save } from 'sonar-ui-common/helpers/storage'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { isSonarCloud } from '../../../../helpers/system'; +import { ComponentQualifier } from '../../../../types/component'; import { AllProjects } from '../AllProjects'; jest.mock('../ProjectsList', () => ({ @@ -212,7 +213,16 @@ function shallowRender( ); wrapper.setState({ loading: false, - projects: [{ key: 'foo', measures: {}, name: 'Foo', tags: [], visibility: 'public' }], + projects: [ + { + key: 'foo', + measures: {}, + name: 'Foo', + qualifier: ComponentQualifier.Project, + tags: [], + visibility: 'public' + } + ], total: 0 }); return wrapper; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx index 0fcbf20e45d..7b74ad36e93 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx @@ -19,11 +19,15 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockCurrentUser } from '../../../../helpers/testMocks'; +import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; +import { ComponentQualifier } from '../../../../types/component'; import { Project } from '../../types'; import ProjectCard from '../ProjectCard'; -const ORGANIZATION = { key: 'org', name: 'org' }; +jest.mock( + 'date-fns/difference_in_milliseconds', + () => () => 1000 * 60 * 60 * 24 * 30 * 8 // ~ 8 months +); const MEASURES = { alert_status: 'OK', @@ -34,36 +38,91 @@ const MEASURES = { const PROJECT: Project = { analysisDate: '2017-01-01', - leakPeriodDate: '2016-12-01', key: 'foo', measures: MEASURES, name: 'Foo', organization: { key: 'org', name: 'org' }, + qualifier: ComponentQualifier.Project, tags: [], visibility: 'public' }; -it('should show <ProjectCardOverall/> by default', () => { - const wrapper = shallowRender(); - expect(wrapper.find('ProjectCardOverall').exists()).toBe(true); - expect(wrapper.find('ProjectCardLeak').exists()).toBe(false); +const USER_LOGGED_OUT = mockCurrentUser(); +const USER_LOGGED_IN = mockLoggedInUser(); + +it('should display analysis date (and not leak period) when defined', () => { + expect( + shallowRender(PROJECT) + .find('.project-card-dates') + .exists() + ).toBe(true); + expect( + shallowRender({ ...PROJECT, analysisDate: undefined }) + .find('.project-card-dates') + .exists() + ).toBe(false); +}); + +it('should not display the quality gate', () => { + const project = { ...PROJECT, analysisDate: undefined }; + expect( + shallowRender(project) + .find('ProjectCardOverallQualityGate') + .exists() + ).toBe(false); +}); + +it('should display tags', () => { + const project = { ...PROJECT, tags: ['foo', 'bar'] }; + expect( + shallowRender(project) + .find('TagsList') + .exists() + ).toBe(true); +}); + +it('should display private badge', () => { + const project: Project = { ...PROJECT, visibility: 'private' }; + expect( + shallowRender(project) + .find('Connect(PrivacyBadge)') + .exists() + ).toBe(true); +}); + +it('should display the overall measures and quality gate', () => { + expect(shallowRender(PROJECT)).toMatchSnapshot(); +}); + +it('should display not analyzed yet', () => { + expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot(); +}); + +it('should display configure analysis button for logged in user', () => { + expect(shallowRender({ ...PROJECT, analysisDate: undefined }, USER_LOGGED_IN)).toMatchSnapshot(); }); -it('should show <ProjectCardLeak/> when asked', () => { - const wrapper = shallowRender('leak'); - expect(wrapper.find('ProjectCardLeak').exists()).toBe(true); - expect(wrapper.find('ProjectCardOverall').exists()).toBe(false); +it('should display applications', () => { + expect( + shallowRender({ ...PROJECT, qualifier: ComponentQualifier.Application }) + ).toMatchSnapshot(); + expect( + shallowRender({ + ...PROJECT, + qualifier: ComponentQualifier.Application, + measures: { ...MEASURES, projects: '3' } + }) + ).toMatchSnapshot('with project count'); }); -function shallowRender(type?: string) { +function shallowRender(project: Project, user: T.CurrentUser = USER_LOGGED_OUT) { return shallow( <ProjectCard - currentUser={mockCurrentUser()} - handleFavorite={jest.fn} - height={200} - organization={ORGANIZATION} - project={PROJECT} - type={type} + currentUser={user} + handleFavorite={jest.fn()} + height={100} + organization={undefined} + project={project} /> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx deleted file mode 100644 index ee5e548d38f..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx +++ /dev/null @@ -1,105 +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 { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; -import { Project } from '../../types'; -import ProjectCardLeak from '../ProjectCardLeak'; - -jest.mock( - 'date-fns/difference_in_milliseconds', - () => () => 1000 * 60 * 60 * 24 * 30 * 8 // ~ 8 months -); - -const MEASURES = { - alert_status: 'OK', - reliability_rating: '1.0', - sqale_rating: '1.0', - new_bugs: '12' -}; - -const PROJECT: Project = { - analysisDate: '2017-01-01', - leakPeriodDate: '2016-12-01', - key: 'foo', - measures: MEASURES, - name: 'Foo', - organization: { key: 'org', name: 'org' }, - tags: [], - visibility: 'public' -}; - -const USER_LOGGED_OUT = mockCurrentUser(); -const USER_LOGGED_IN = mockLoggedInUser(); - -it('should display analysis date and leak start date', () => { - const card = shallowRender(PROJECT); - expect(card.find('.project-card-dates').exists()).toBe(true); - expect(card.find('.project-card-dates').find('.project-card-leak-date')).toHaveLength(1); - expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1); -}); - -it('should not display analysis date or leak start date', () => { - const project = { ...PROJECT, analysisDate: undefined }; - const card = shallowRender(project); - expect(card.find('.project-card-dates').exists()).toBe(false); -}); - -it('should display tags', () => { - const project = { ...PROJECT, tags: ['foo', 'bar'] }; - expect( - shallowRender(project) - .find('TagsList') - .exists() - ).toBe(true); -}); - -it('should display private badge', () => { - const project: Project = { ...PROJECT, visibility: 'private' }; - expect( - shallowRender(project) - .find('Connect(PrivacyBadge)') - .exists() - ).toBe(true); -}); - -it('should display the leak measures and quality gate', () => { - expect(shallowRender(PROJECT)).toMatchSnapshot(); -}); - -it('should display not analyzed yet', () => { - expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot(); -}); - -it('should display configure analysis button for logged in user', () => { - expect(shallowRender({ ...PROJECT, analysisDate: undefined }, USER_LOGGED_IN)).toMatchSnapshot(); -}); - -function shallowRender(project: Project, user: T.CurrentUser = USER_LOGGED_OUT) { - return shallow( - <ProjectCardLeak - currentUser={user} - handleFavorite={jest.fn()} - height={100} - organization={undefined} - project={project} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx deleted file mode 100644 index 369c72ba783..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx +++ /dev/null @@ -1,108 +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 { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; -import { Project } from '../../types'; -import ProjectCardOverall from '../ProjectCardOverall'; - -const MEASURES = { - alert_status: 'OK', - reliability_rating: '1.0', - sqale_rating: '1.0', - new_bugs: '12' -}; - -const PROJECT: Project = { - analysisDate: '2017-01-01', - key: 'foo', - measures: MEASURES, - name: 'Foo', - organization: { key: 'org', name: 'org' }, - tags: [], - visibility: 'public' -}; - -const USER_LOGGED_OUT = mockCurrentUser(); -const USER_LOGGED_IN = mockLoggedInUser(); - -it('should display analysis date (and not leak period) when defined', () => { - expect( - shallowRender(PROJECT) - .find('.project-card-dates') - .exists() - ).toBe(true); - expect( - shallowRender({ ...PROJECT, analysisDate: undefined }) - .find('.project-card-dates') - .exists() - ).toBe(false); -}); - -it('should not display the quality gate', () => { - const project = { ...PROJECT, analysisDate: undefined }; - expect( - shallowRender(project) - .find('ProjectCardOverallQualityGate') - .exists() - ).toBe(false); -}); - -it('should display tags', () => { - const project = { ...PROJECT, tags: ['foo', 'bar'] }; - expect( - shallowRender(project) - .find('TagsList') - .exists() - ).toBe(true); -}); - -it('should display private badge', () => { - const project: Project = { ...PROJECT, visibility: 'private' }; - expect( - shallowRender(project) - .find('Connect(PrivacyBadge)') - .exists() - ).toBe(true); -}); - -it('should display the overall measures and quality gate', () => { - expect(shallowRender(PROJECT)).toMatchSnapshot(); -}); - -it('should display not analyzed yet', () => { - expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot(); -}); - -it('should display configure analysis button for logged in user', () => { - expect(shallowRender({ ...PROJECT, analysisDate: undefined }, USER_LOGGED_IN)).toMatchSnapshot(); -}); - -function shallowRender(project: Project, user: T.CurrentUser = USER_LOGGED_OUT) { - return shallow( - <ProjectCardOverall - currentUser={user} - handleFavorite={jest.fn()} - height={100} - organization={undefined} - project={project} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx index 736a00ca4ef..4516ca937b6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx @@ -19,69 +19,53 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ComponentQualifier } from '../../../../types/component'; import ProjectCardOverallMeasures from '../ProjectCardOverallMeasures'; it('should render correctly with all data', () => { - const measures = { - alert_status: 'ERROR', - bugs: '17', - code_smells: '132', - coverage: '88.3', - duplicated_lines_density: '9.8', - ncloc: '2053', - reliability_rating: '1.0', - security_rating: '1.0', - sqale_rating: '1.0', - vulnerabilities: '0' - }; - const wrapper = shallow(<ProjectCardOverallMeasures measures={measures} />); - expect(wrapper).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); }); it('should not render coverage', () => { - const measures = { - alert_status: 'ERROR', - bugs: '17', - code_smells: '132', - duplicated_lines_density: '9.8', - ncloc: '2053', - reliability_rating: '1.0', - security_rating: '1.0', - sqale_rating: '1.0', - vulnerabilities: '0' - }; - const wrapper = shallow(<ProjectCardOverallMeasures measures={measures} />); - expect(wrapper.find('[data-key="coverage"]').exists()).toBe(false); + expect( + shallowRender({ coverage: undefined }) + .find('[data-key="coverage"]') + .exists() + ).toBe(false); }); it('should render empty', () => { - const measures = { - alert_status: 'ERROR', - bugs: '17', - code_smells: '132', - coverage: '88.3', - duplicated_lines_density: '9.8', - reliability_rating: '1.0', - security_rating: '1.0', - sqale_rating: '1.0', - vulnerabilities: '0' - }; - expect(shallow(<ProjectCardOverallMeasures measures={measures} />)).toMatchSnapshot(); + expect(shallowRender({ ncloc: undefined })).toMatchSnapshot('project'); + expect(shallowRender({ ncloc: undefined }, ComponentQualifier.Application)).toMatchSnapshot( + 'application' + ); }); it('should render ncloc correctly', () => { + expect(shallowRender({ ncloc: '16549887' }).find('[data-key="ncloc"]')).toMatchSnapshot(); +}); + +function shallowRender( + overriddenMeasures: T.Dict<string | undefined> = {}, + componentQualifier?: ComponentQualifier +) { const measures = { alert_status: 'ERROR', bugs: '17', code_smells: '132', coverage: '88.3', - ncloc: '16549887', duplicated_lines_density: '9.8', + ncloc: '2053', reliability_rating: '1.0', security_rating: '1.0', sqale_rating: '1.0', - vulnerabilities: '0' + vulnerabilities: '0', + ...overriddenMeasures }; - const wrapper = shallow(<ProjectCardOverallMeasures measures={measures} />); - expect(wrapper.find('[data-key="ncloc"]')).toMatchSnapshot(); -}); + return shallow( + <ProjectCardOverallMeasures + componentQualifier={componentQualifier ?? ComponentQualifier.Project} + measures={measures} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index ecffd54942a..58f1e3f2750 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -6,6 +6,7 @@ Array [ "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, @@ -19,6 +20,7 @@ Array [ "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, @@ -75,6 +77,7 @@ exports[`renders 1`] = ` "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, @@ -132,6 +135,7 @@ exports[`renders 1`] = ` "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, @@ -225,6 +229,7 @@ exports[`renders 2`] = ` "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, @@ -254,6 +259,7 @@ exports[`renders 2`] = ` "key": "foo", "measures": Object {}, "name": "Foo", + "qualifier": "TRK", "tags": Array [], "visibility": "public", }, diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.tsx.snap new file mode 100644 index 00000000000..9232fe2c56c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.tsx.snap @@ -0,0 +1,455 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display applications 1`] = ` +<div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key="foo" + style={ + Object { + "height": 100, + } + } +> + <div> + <div + className="project-card-header" + > + <h2 + className="project-card-name" + > + <Connect(ProjectCardOrganization) + organization={ + Object { + "key": "org", + "name": "org", + } + } + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + Foo + </Link> + </h2> + <ProjectCardQualityGate + status="OK" + /> + <div + className="project-card-header-right" + > + <Connect(PrivacyBadge) + className="spacer-left" + organization={ + Object { + "key": "org", + "name": "org", + } + } + qualifier="APP" + tooltipProps={ + Object { + "projectKey": "foo", + } + } + visibility="public" + /> + </div> + </div> + <div + className="display-flex-center project-card-dates spacer-top" + > + <DateTimeFormatter + date="2017-01-01" + > + <Component /> + </DateTimeFormatter> + <div + className="text-right flex-1-0-auto" + > + <QualifierIcon + className="spacer-right" + qualifier="APP" + /> + qualifier.APP + </div> + </div> + </div> + <ProjectCardOverallMeasures + componentQualifier="APP" + measures={ + Object { + "alert_status": "OK", + "new_bugs": "12", + "reliability_rating": "1.0", + "sqale_rating": "1.0", + } + } + /> +</div> +`; + +exports[`should display applications: with project count 1`] = ` +<div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key="foo" + style={ + Object { + "height": 100, + } + } +> + <div> + <div + className="project-card-header" + > + <h2 + className="project-card-name" + > + <Connect(ProjectCardOrganization) + organization={ + Object { + "key": "org", + "name": "org", + } + } + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + Foo + </Link> + </h2> + <ProjectCardQualityGate + status="OK" + /> + <div + className="project-card-header-right" + > + <Connect(PrivacyBadge) + className="spacer-left" + organization={ + Object { + "key": "org", + "name": "org", + } + } + qualifier="APP" + tooltipProps={ + Object { + "projectKey": "foo", + } + } + visibility="public" + /> + </div> + </div> + <div + className="display-flex-center project-card-dates spacer-top" + > + <DateTimeFormatter + date="2017-01-01" + > + <Component /> + </DateTimeFormatter> + <div + className="text-right flex-1-0-auto" + > + <QualifierIcon + className="spacer-right" + qualifier="APP" + /> + qualifier.APP + ‒ + x_projects_.3 + </div> + </div> + </div> + <ProjectCardOverallMeasures + componentQualifier="APP" + measures={ + Object { + "alert_status": "OK", + "new_bugs": "12", + "projects": "3", + "reliability_rating": "1.0", + "sqale_rating": "1.0", + } + } + /> +</div> +`; + +exports[`should display configure analysis button for logged in user 1`] = ` +<div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key="foo" + style={ + Object { + "height": 100, + } + } +> + <div> + <div + className="project-card-header" + > + <h2 + className="project-card-name" + > + <Connect(ProjectCardOrganization) + organization={ + Object { + "key": "org", + "name": "org", + } + } + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + Foo + </Link> + </h2> + <div + className="project-card-header-right" + > + <Connect(PrivacyBadge) + className="spacer-left" + organization={ + Object { + "key": "org", + "name": "org", + } + } + qualifier="TRK" + tooltipProps={ + Object { + "projectKey": "foo", + } + } + visibility="public" + /> + </div> + </div> + </div> + <div + className="project-card-not-analyzed" + > + <span + className="note" + > + projects.not_analyzed.TRK + </span> + <Link + className="button spacer-left" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + projects.configure_analysis + </Link> + </div> +</div> +`; + +exports[`should display not analyzed yet 1`] = ` +<div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key="foo" + style={ + Object { + "height": 100, + } + } +> + <div> + <div + className="project-card-header" + > + <h2 + className="project-card-name" + > + <Connect(ProjectCardOrganization) + organization={ + Object { + "key": "org", + "name": "org", + } + } + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + Foo + </Link> + </h2> + <div + className="project-card-header-right" + > + <Connect(PrivacyBadge) + className="spacer-left" + organization={ + Object { + "key": "org", + "name": "org", + } + } + qualifier="TRK" + tooltipProps={ + Object { + "projectKey": "foo", + } + } + visibility="public" + /> + </div> + </div> + </div> + <div + className="project-card-not-analyzed" + > + <span + className="note" + > + projects.not_analyzed.TRK + </span> + </div> +</div> +`; + +exports[`should display the overall measures and quality gate 1`] = ` +<div + className="boxed-group project-card big-padded display-flex-column display-flex-space-between" + data-key="foo" + style={ + Object { + "height": 100, + } + } +> + <div> + <div + className="project-card-header" + > + <h2 + className="project-card-name" + > + <Connect(ProjectCardOrganization) + organization={ + Object { + "key": "org", + "name": "org", + } + } + /> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "foo", + }, + } + } + > + Foo + </Link> + </h2> + <ProjectCardQualityGate + status="OK" + /> + <div + className="project-card-header-right" + > + <Connect(PrivacyBadge) + className="spacer-left" + organization={ + Object { + "key": "org", + "name": "org", + } + } + qualifier="TRK" + tooltipProps={ + Object { + "projectKey": "foo", + } + } + visibility="public" + /> + </div> + </div> + <div + className="display-flex-center project-card-dates spacer-top" + > + <DateTimeFormatter + date="2017-01-01" + > + <Component /> + </DateTimeFormatter> + </div> + </div> + <ProjectCardOverallMeasures + componentQualifier="TRK" + measures={ + Object { + "alert_status": "OK", + "new_bugs": "12", + "reliability_rating": "1.0", + "sqale_rating": "1.0", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap deleted file mode 100644 index fb8431450f3..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap +++ /dev/null @@ -1,275 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display configure analysis button for logged in user 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - </div> - <div - className="boxed-group-inner" - > - <div - className="project-card-not-analyzed" - > - <span - className="note" - > - projects.not_analyzed - </span> - <Link - className="button spacer-left" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, - } - } - > - projects.configure_analysis - </Link> - </div> - </div> -</div> -`; - -exports[`should display not analyzed yet 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - </div> - <div - className="boxed-group-inner" - > - <div - className="project-card-not-analyzed" - > - <span - className="note" - > - projects.not_analyzed - </span> - </div> - </div> -</div> -`; - -exports[`should display the leak measures and quality gate 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <ProjectCardQualityGate - status="OK" - /> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - <div - className="project-card-dates note text-right pull-right" - > - <span - className="project-card-leak-date pull-right" - > - projects.new_code_period_x.duration.months.8 - </span> - <DateTimeFormatter - date="2017-01-01" - > - <Component /> - </DateTimeFormatter> - </div> - </div> - <div - className="boxed-group-inner" - > - <ProjectCardLeakMeasures - measures={ - Object { - "alert_status": "OK", - "new_bugs": "12", - "reliability_rating": "1.0", - "sqale_rating": "1.0", - } - } - /> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap deleted file mode 100644 index 0c70453a77f..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap +++ /dev/null @@ -1,273 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display configure analysis button for logged in user 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - </div> - <div - className="boxed-group-inner" - > - <div - className="project-card-not-analyzed" - > - <span - className="note" - > - projects.not_analyzed - </span> - <Link - className="button spacer-left" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, - } - } - > - projects.configure_analysis - </Link> - </div> - </div> -</div> -`; - -exports[`should display not analyzed yet 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - </div> - <div - className="boxed-group-inner" - > - <div - className="project-card-not-analyzed" - > - <span - className="note" - > - projects.not_analyzed - </span> - </div> - </div> -</div> -`; - -exports[`should display the overall measures and quality gate 1`] = ` -<div - className="boxed-group project-card" - data-key="foo" - style={ - Object { - "height": 100, - } - } -> - <div - className="boxed-group-header clearfix" - > - <div - className="project-card-header" - > - <h2 - className="project-card-name" - > - <Connect(ProjectCardOrganization) - organization={ - Object { - "key": "org", - "name": "org", - } - } - /> - <Link - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "foo", - }, - } - } - > - Foo - </Link> - </h2> - <ProjectCardQualityGate - status="OK" - /> - <div - className="project-card-header-right" - > - <Connect(PrivacyBadge) - className="spacer-left" - organization={ - Object { - "key": "org", - "name": "org", - } - } - qualifier="TRK" - tooltipProps={ - Object { - "projectKey": "foo", - } - } - visibility="public" - /> - </div> - </div> - <div - className="project-card-dates note text-right" - > - <DateTimeFormatter - date="2017-01-01" - > - <Component /> - </DateTimeFormatter> - </div> - </div> - <div - className="boxed-group-inner" - > - <ProjectCardOverallMeasures - measures={ - Object { - "alert_status": "OK", - "new_bugs": "12", - "reliability_rating": "1.0", - "sqale_rating": "1.0", - } - } - /> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap index 23088af1a46..69a88c913e4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap @@ -180,9 +180,17 @@ exports[`should render correctly with all data 1`] = ` </div> `; -exports[`should render empty 1`] = ` +exports[`should render empty: application 1`] = ` <div - className="note" + className="note big-spacer-top" +> + portfolio.app.empty +</div> +`; + +exports[`should render empty: project 1`] = ` +<div + className="note big-spacer-top" > overview.project.main_branch_empty </div> diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index e263c6257b3..7cd2e2a6216 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -77,12 +77,6 @@ white-space: nowrap; } -.project-card-dates { - width: 100%; - margin-top: 10px; - margin-bottom: -10px; -} - .project-card-leak-date { padding: 4px 8px; margin: -5px -4px -5px 24px; diff --git a/server/sonar-web/src/main/js/apps/projects/types.ts b/server/sonar-web/src/main/js/apps/projects/types.ts index 1bd13d97339..566e8496b1d 100644 --- a/server/sonar-web/src/main/js/apps/projects/types.ts +++ b/server/sonar-web/src/main/js/apps/projects/types.ts @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ComponentQualifier } from '../../types/component'; + export interface Project { analysisDate?: string; isFavorite?: boolean; @@ -25,6 +27,8 @@ export interface Project { measures: T.Dict<string>; name: string; organization?: { key: string; name: string }; + projects?: number; + qualifier: ComponentQualifier; tags: string[]; visibility: T.Visibility; } diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index 926d048366e..445c5119601 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -24,6 +24,7 @@ import { Facet, searchProjects } from '../../api/components'; import { getMeasuresForProjects } from '../../api/measures'; import { getOrganizations } from '../../api/organizations'; import { getPeriodValue, isDiffMetric } from '../../helpers/measures'; +import { MetricKey } from '../../types/metrics'; import { convertToFilter, Query } from './query'; interface SortingOption { @@ -92,44 +93,67 @@ const PAGE_SIZE = 50; const PAGE_SIZE_VISUALIZATIONS = 99; const METRICS = [ - 'alert_status', - 'bugs', - 'reliability_rating', - 'vulnerabilities', - 'security_rating', - 'security_hotspots_reviewed', - 'security_review_rating', - 'code_smells', - 'sqale_rating', - 'duplicated_lines_density', - 'coverage', - 'ncloc', - 'ncloc_language_distribution' + MetricKey.alert_status, + MetricKey.bugs, + MetricKey.reliability_rating, + MetricKey.vulnerabilities, + MetricKey.security_rating, + MetricKey.security_hotspots_reviewed, + MetricKey.security_review_rating, + MetricKey.code_smells, + MetricKey.sqale_rating, + MetricKey.duplicated_lines_density, + MetricKey.coverage, + MetricKey.ncloc, + MetricKey.ncloc_language_distribution, + MetricKey.projects ]; const LEAK_METRICS = [ - 'alert_status', - 'new_bugs', - 'new_reliability_rating', - 'new_vulnerabilities', - 'new_security_rating', - 'new_security_hotspots_reviewed', - 'new_security_review_rating', - 'new_code_smells', - 'new_maintainability_rating', - 'new_coverage', - 'new_duplicated_lines_density', - 'new_lines' + MetricKey.alert_status, + MetricKey.new_bugs, + MetricKey.new_reliability_rating, + MetricKey.new_vulnerabilities, + MetricKey.new_security_rating, + MetricKey.new_security_hotspots_reviewed, + MetricKey.new_security_review_rating, + MetricKey.new_code_smells, + MetricKey.new_maintainability_rating, + MetricKey.new_coverage, + MetricKey.new_duplicated_lines_density, + MetricKey.new_lines, + MetricKey.projects ]; const METRICS_BY_VISUALIZATION: T.Dict<string[]> = { - risk: ['reliability_rating', 'security_rating', 'coverage', 'ncloc', 'sqale_index'], + risk: [ + MetricKey.reliability_rating, + MetricKey.security_rating, + MetricKey.coverage, + MetricKey.ncloc, + MetricKey.sqale_index + ], // x, y, size, color - reliability: ['ncloc', 'reliability_remediation_effort', 'bugs', 'reliability_rating'], - security: ['ncloc', 'security_remediation_effort', 'vulnerabilities', 'security_rating'], - maintainability: ['ncloc', 'sqale_index', 'code_smells', 'sqale_rating'], - coverage: ['complexity', 'coverage', 'uncovered_lines'], - duplications: ['ncloc', 'duplicated_lines_density', 'duplicated_blocks'] + reliability: [ + MetricKey.ncloc, + MetricKey.reliability_remediation_effort, + MetricKey.bugs, + MetricKey.reliability_rating + ], + security: [ + MetricKey.ncloc, + MetricKey.security_remediation_effort, + MetricKey.vulnerabilities, + MetricKey.security_rating + ], + maintainability: [ + MetricKey.ncloc, + MetricKey.sqale_index, + MetricKey.code_smells, + MetricKey.sqale_rating + ], + coverage: [MetricKey.complexity, MetricKey.coverage, MetricKey.uncovered_lines], + duplications: [MetricKey.ncloc, MetricKey.duplicated_lines_density, MetricKey.duplicated_blocks] }; export const FACETS = [ diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx index 445e872382f..161202d0c6b 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx @@ -20,11 +20,13 @@ import * as React from 'react'; import BubbleChart from 'sonar-ui-common/components/charts/BubbleChart'; import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; +import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import { RATING_COLORS } from '../../../helpers/constants'; import { getProjectUrl } from '../../../helpers/urls'; +import { ComponentQualifier } from '../../../types/component'; import { Project } from '../types'; const X_METRIC = 'sqale_index'; @@ -77,7 +79,20 @@ export default class Risk extends React.PureComponent<Props> { return ( <div className="text-left"> - <div className="little-spacer-bottom">{fullProjectName}</div> + <div className="little-spacer-bottom display-flex-center display-flex-space-between"> + {fullProjectName} + + {project.qualifier === ComponentQualifier.Application && ( + <div className="big-spacer-left nowrap"> + <QualifierIcon + className="little-spacer-right" + fill="currentColor" + qualifier={ComponentQualifier.Application} + /> + {translate('qualifier.APP')} + </div> + )} + </div> {this.getMetricTooltip({ key: COLOR_METRIC_1, type: COLOR_METRIC_TYPE }, color1)} {this.getMetricTooltip({ key: COLOR_METRIC_2, type: COLOR_METRIC_TYPE }, color2)} {this.getMetricTooltip({ key: Y_METRIC, type: Y_METRIC_TYPE }, y)} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx index a4fb6562a0c..55f0ec925dd 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx @@ -20,11 +20,13 @@ import * as React from 'react'; import BubbleChart from 'sonar-ui-common/components/charts/BubbleChart'; import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; +import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import { RATING_COLORS } from '../../../helpers/constants'; import { getProjectUrl } from '../../../helpers/urls'; +import { ComponentQualifier } from '../../../types/component'; import { Project } from '../types'; interface Metric { @@ -71,7 +73,20 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> { return ( <div className="text-left"> - <div className="little-spacer-bottom">{fullProjectName}</div> + <div className="little-spacer-bottom display-flex-center display-flex-space-between"> + {fullProjectName} + + {project.qualifier === ComponentQualifier.Application && ( + <div className="big-spacer-left nowrap"> + <QualifierIcon + className="little-spacer-right" + fill="currentColor" + qualifier={ComponentQualifier.Application} + /> + {translate('qualifier.APP')} + </div> + )} + </div> {this.getMetricTooltip(this.props.xMetric, x)} {this.getMetricTooltip(this.props.yMetric, y)} {this.getMetricTooltip(this.props.sizeMetric, size)} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx index 07f7327c7eb..b212b3be785 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ComponentQualifier } from '../../../../types/component'; import { Project } from '../../types'; import Risk from '../Risk'; @@ -27,6 +28,7 @@ it('renders', () => { key: 'foo', measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' }, name: 'Foo', + qualifier: ComponentQualifier.Project, tags: [], visibility: 'public' }; diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx index c8b76798e4f..00a64d12e9e 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ComponentQualifier } from '../../../../types/component'; import { Project } from '../../types'; import SimpleBubbleChart from '../SimpleBubbleChart'; @@ -27,16 +28,24 @@ it('renders', () => { key: 'foo', measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' }, name: 'Foo', + qualifier: ComponentQualifier.Project, tags: [], visibility: 'public' }; + const app = { + ...project1, + key: 'app', + measures: { complexity: '23.1', coverage: '87.3', ncloc: '32478', security_rating: '1' }, + name: 'App', + qualifier: ComponentQualifier.Application + }; expect( shallow( <SimpleBubbleChart colorMetric="security_rating" displayOrganizations={false} helpText="foobar" - projects={[project1]} + projects={[app, project1]} sizeMetric={{ key: 'ncloc', type: 'INT' }} xMetric={{ key: 'complexity', type: 'INT' }} yMetric={{ key: 'coverage', type: 'PERCENT' }} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap index 38408926a5b..381818cef6b 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap @@ -27,7 +27,7 @@ exports[`renders 1`] = ` className="text-left" > <div - className="little-spacer-bottom" + className="little-spacer-bottom display-flex-center display-flex-space-between" > <strong> Foo diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap index fd133225a39..7d0403e0300 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap @@ -13,6 +13,61 @@ exports[`renders 1`] = ` items={ Array [ Object { + "color": "#00aa00", + "key": "app", + "link": Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "app", + }, + }, + "size": 32478, + "tooltip": <div + className="text-left" + > + <div + className="little-spacer-bottom display-flex-center display-flex-space-between" + > + <strong> + App + </strong> + <div + className="big-spacer-left nowrap" + > + <QualifierIcon + className="little-spacer-right" + fill="currentColor" + qualifier="APP" + /> + qualifier.APP + </div> + </div> + <div> + metric.complexity.name + : + 23 + </div> + <div> + metric.coverage.name + : + 87.3% + </div> + <div> + metric.ncloc.name + : + 32,478 + </div> + <div> + metric.security_rating.name + : + A + </div> + </div>, + "x": 23.1, + "y": 87.3, + }, + Object { "color": "#b0d513", "key": "foo", "link": Object { @@ -27,7 +82,7 @@ exports[`renders 1`] = ` className="text-left" > <div - className="little-spacer-bottom" + className="little-spacer-bottom display-flex-center display-flex-space-between" > <strong> Foo |