Browse Source

SONAR-13188 Include apps in projects list

tags/8.3.0.34182
Jeremy Davis 4 years ago
parent
commit
f535e9b3fc
26 changed files with 935 additions and 1117 deletions
  1. 2
    0
      server/sonar-web/src/main/js/api/components.ts
  2. 8
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  3. 162
    8
      server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx
  4. 0
    124
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
  5. 0
    112
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
  6. 10
    2
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
  7. 11
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
  8. 77
    18
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx
  9. 0
    105
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
  10. 0
    108
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
  11. 28
    44
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx
  12. 6
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
  13. 455
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.tsx.snap
  14. 0
    275
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
  15. 0
    273
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
  16. 10
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
  17. 0
    6
      server/sonar-web/src/main/js/apps/projects/styles.css
  18. 4
    0
      server/sonar-web/src/main/js/apps/projects/types.ts
  19. 55
    31
      server/sonar-web/src/main/js/apps/projects/utils.ts
  20. 16
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx
  21. 16
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx
  22. 2
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
  23. 10
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
  24. 1
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
  25. 56
    1
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
  26. 6
    3
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 0
server/sonar-web/src/main/js/api/components.ts View File

@@ -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;

+ 8
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -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;
}

+ 162
- 8
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx View File

@@ -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>
);
}

+ 0
- 124
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx View File

@@ -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>
);
}
}

+ 0
- 112
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx View File

@@ -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>
);
}
}

+ 10
- 2
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx View File

@@ -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 (

+ 11
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx View File

@@ -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;

+ 77
- 18
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx View File

@@ -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}
/>
);
}

+ 0
- 105
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx View File

@@ -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}
/>
);
}

+ 0
- 108
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx View File

@@ -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}
/>
);
}

+ 28
- 44
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverallMeasures-test.tsx View File

@@ -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}
/>
);
}

+ 6
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap View File

@@ -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",
},

+ 455
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.tsx.snap View File

@@ -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>
`;

+ 0
- 275
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap View File

@@ -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>
`;

+ 0
- 273
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap View File

@@ -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>
`;

+ 10
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap View File

@@ -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>

+ 0
- 6
server/sonar-web/src/main/js/apps/projects/styles.css View File

@@ -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;

+ 4
- 0
server/sonar-web/src/main/js/apps/projects/types.ts View File

@@ -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;
}

+ 55
- 31
server/sonar-web/src/main/js/apps/projects/utils.ts View File

@@ -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 = [

+ 16
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx View File

@@ -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)}

+ 16
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx View File

@@ -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)}

+ 2
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx View File

@@ -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'
};

+ 10
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx View File

@@ -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' }}

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap View File

@@ -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

+ 56
- 1
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap View File

@@ -12,6 +12,61 @@ exports[`renders 1`] = `
height={600}
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",
@@ -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

+ 6
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -142,6 +142,7 @@ project=Project
project_x=Project: {0}
projects=Projects
projects_=project(s)
x_projects_={0} project(s)
project_singular=project
project_plural=projects
projects_management=Projects Management
@@ -894,8 +895,10 @@ projects.no_favorite_projects.how_to_add_projects=Here is how to add projects to
projects.no_favorite_projects.favorite_projects_from_orgs=Favorite projects from your orgs
projects.no_favorite_projects.favorite_public_projects=Favorite public projects
projects.explore_projects=Explore Projects
projects.not_analyzed=Project is not analyzed yet.
projects.no_new_code_period=Project has no new code data yet.
projects.not_analyzed.TRK=Project is not analyzed yet.
projects.not_analyzed.APP=None of the Application's projects have been analyzed.
projects.no_new_code_period.TRK=Project has no new code data yet.
projects.no_new_code_period.APP=Application has no new code data yet.
projects.new_code_period_x=New code: last {0}
projects.configure_analysis=Configure analysis
projects.last_analysis_on_x=Last analysis: {0}
@@ -2913,7 +2916,7 @@ component_measures.bubble_chart.zoom_level=Current zoom level. Scroll on the cha
# ABOUT PAGE
#
#------------------------------------------------------------------------------
about_page.projects_analyzed=Projects Analyzed
about_page.projects_analyzed=Projects
about_page.read_more=Read More
about_page.read_documentation=Read documentation


Loading…
Cancel
Save