Bläddra i källkod

MMF-721 Visualizations on the Projects page (#1826)

tags/6.4-RC1
Stas Vilchik 7 år sedan
förälder
incheckning
6d2b71500b
34 ändrade filer med 1415 tillägg och 157 borttagningar
  1. 0
    1
      server/sonar-web/.eslintrc
  2. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
  3. 6
    0
      server/sonar-web/src/main/js/apps/component-measures/styles.css
  4. 60
    14
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  5. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js
  6. 2
    1
      server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js
  7. 10
    6
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
  8. 19
    12
      server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
  9. 4
    1
      server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
  10. 50
    0
      server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js
  11. 33
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js
  12. 26
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js
  13. 22
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
  14. 19
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap
  15. 30
    9
      server/sonar-web/src/main/js/apps/projects/store/actions.js
  16. 11
    1
      server/sonar-web/src/main/js/apps/projects/store/utils.js
  17. 22
    0
      server/sonar-web/src/main/js/apps/projects/styles.css
  18. 15
    0
      server/sonar-web/src/main/js/apps/projects/utils.js
  19. 36
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js
  20. 36
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js
  21. 35
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js
  22. 133
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js
  23. 125
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js
  24. 36
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js
  25. 94
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js
  26. 44
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js
  27. 57
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js
  28. 36
    0
      server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js
  29. 116
    93
      server/sonar-web/src/main/js/components/charts/BubbleChart.js
  30. 4
    16
      server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.js
  31. 311
    0
      server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.js.snap
  32. 2
    0
      server/sonar-web/src/main/js/helpers/constants.js
  33. 0
    1
      server/sonar-web/src/main/less/components/react-select.less
  34. 18
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 1
server/sonar-web/.eslintrc Visa fil

@@ -136,7 +136,6 @@
"jsx-a11y/tabindex-no-positive": 2,

"react/jsx-boolean-value": [2, "always"],
"react/jsx-closing-bracket-location": 2,
"react/jsx-curly-spacing": [2, "never"],
"react/jsx-equals-spacing": [2, "never"],
"react/jsx-key": 2,

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js Visa fil

@@ -19,7 +19,7 @@
*/
import React from 'react';
import Spinner from './../Spinner';
import { BubbleChart as OriginalBubbleChart } from '../../../../components/charts/bubble-chart';
import OriginalBubbleChart from '../../../../components/charts/BubbleChart';
import bubbles from '../../config/bubbles';
import { getComponentLeaves } from '../../../../api/components';
import { formatMeasure } from '../../../../helpers/measures';

+ 6
- 0
server/sonar-web/src/main/js/apps/component-measures/styles.css Visa fil

@@ -320,6 +320,9 @@
.measure-details-bubble-chart-axis.x {
left: 50%;
bottom: 10px;
width: 500px;
margin-left: -250px;
text-align: center;
}

.measure-details-bubble-chart-axis.y {
@@ -331,6 +334,9 @@
.measure-details-bubble-chart-axis.size {
left: 50%;
top: 10px;
width: 500px;
margin-left: -250px;
text-align: center;
}

.measure-details-treemap {

+ 60
- 14
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js Visa fil

@@ -22,13 +22,17 @@ import PageHeaderContainer from './PageHeaderContainer';
import ProjectsListContainer from './ProjectsListContainer';
import ProjectsListFooterContainer from './ProjectsListFooterContainer';
import PageSidebar from './PageSidebar';
import VisualizationsContainer from '../visualizations/VisualizationsContainer';
import { parseUrlQuery } from '../store/utils';
import { getProjectUrl } from '../../../helpers/urls';

export default class AllProjects extends React.Component {
static propTypes = {
isFavorite: React.PropTypes.bool.isRequired,
location: React.PropTypes.object.isRequired,
fetchProjects: React.PropTypes.func.isRequired,
organization: React.PropTypes.object
organization: React.PropTypes.object,
router: React.PropTypes.object.isRequired
};

state = {
@@ -56,8 +60,41 @@ export default class AllProjects extends React.Component {
this.props.fetchProjects(query, this.props.isFavorite, this.props.organization);
}

handleViewChange = view => {
const query = {
...this.props.location.query,
view: view === 'list' ? undefined : view
};
if (query.view !== 'visualizations') {
Object.assign(query, { visualization: undefined });
}
this.props.router.push({
pathname: this.props.location.pathname,
query
});
};

handleVisualizationChange = visualization => {
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...this.props.location.query,
view: 'visualizations',
visualization
}
});
};

handleProjectOpen = projectKey => {
this.props.router.push(getProjectUrl(projectKey));
};

render() {
const isFiltered = Object.keys(this.state.query).some(key => this.state.query[key] != null);
const { query } = this.state;
const isFiltered = Object.keys(query).some(key => query[key] != null);

const view = query.view || 'list';
const visualization = query.visualization || 'quality';

const top = this.props.organization ? 95 : 30;

@@ -66,24 +103,33 @@ export default class AllProjects extends React.Component {
<aside className="page-sidebar-fixed page-sidebar-sticky projects-sidebar">
<div className="page-sidebar-sticky-inner" style={{ top }}>
<PageSidebar
query={this.state.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>
</aside>
<div className="page-main">
<PageHeaderContainer />
<ProjectsListContainer
isFavorite={this.props.isFavorite}
isFiltered={isFiltered}
organization={this.props.organization}
/>
<ProjectsListFooterContainer
query={this.state.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
{view === 'list' &&
<ProjectsListContainer
isFavorite={this.props.isFavorite}
isFiltered={isFiltered}
organization={this.props.organization}
/>}
{view === 'list' &&
<ProjectsListFooterContainer
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>}
{view === 'visualizations' &&
<VisualizationsContainer
onProjectOpen={this.handleProjectOpen}
onVisualizationChange={this.handleVisualizationChange}
sort={query.sort}
visualization={visualization}
/>}
</div>
</div>
);

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js Visa fil

@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import AllProjects from './AllProjects';
import { fetchProjects } from '../store/actions';

export default connect(null, { fetchProjects })(AllProjects);
export default connect(null, { fetchProjects })(withRouter(AllProjects));

+ 2
- 1
server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js Visa fil

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import AllProjects from './AllProjects';
import { fetchProjects } from '../store/actions';
import { getCurrentUser } from '../../../store/rootReducer';
@@ -27,4 +28,4 @@ const mapStateToProps = state => ({
isFavorite: true
});

export default connect(mapStateToProps, { fetchProjects })(AllProjects);
export default connect(mapStateToProps, { fetchProjects })(withRouter(AllProjects));

+ 10
- 6
server/sonar-web/src/main/js/apps/projects/components/PageHeader.js Visa fil

@@ -17,22 +17,26 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import ViewSelect from './ViewSelect';
import { translate } from '../../../helpers/l10n';

export default class PageHeader extends React.Component {
static propTypes = {
loading: React.PropTypes.bool,
total: React.PropTypes.number
props: {
loading: boolean,
onViewChange: (string) => void,
total?: number,
view: string
};

render() {
const { loading } = this.props;

return (
<header className="page-header">
<ViewSelect onChange={this.props.onViewChange} view={this.props.view} />

<div className="page-actions projects-page-actions">
{!!loading && <i className="spinner spacer-right" />}
{!!this.props.loading && <i className="spinner spacer-right" />}

{this.props.total != null &&
<span>

+ 19
- 12
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js Visa fil

@@ -40,12 +40,19 @@ export default class PageSidebar extends React.PureComponent {
};

render() {
const isFiltered = Object.keys(this.props.query).some(key => this.props.query[key] != null);
const { query } = this.props;

const isFiltered = Object.keys(query)
.filter(key => key !== 'view' && key !== 'visualization')
.some(key => query[key] != null);

const basePathName = this.props.organization
? `/organizations/${this.props.organization.key}/projects`
: '/projects';
const pathname = basePathName + (this.props.isFavorite ? '/favorite' : '');
const linkQuery = query.view === 'visualizations'
? { view: query.view, visualization: query.visualization }
: undefined;

return (
<div className="search-navigator-facets-list">
@@ -54,61 +61,61 @@ export default class PageSidebar extends React.PureComponent {
<div className="projects-facets-header clearfix">
{isFiltered &&
<div className="projects-facets-reset">
<Link to={pathname} className="button button-red">
<Link to={{ pathname, query: linkQuery }} className="button button-red">
{translate('projects.clear_all_filters')}
</Link>
</div>}

<h3>{translate('filters')}</h3>
<SearchFilterContainer
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>

<QualityGateFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<ReliabilityFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<SecurityFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<MaintainabilityFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<CoverageFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<DuplicationsFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<SizeFilter
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<LanguagesFilterContainer
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<TagsFilterContainer
query={this.props.query}
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>

+ 4
- 1
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js Visa fil

@@ -50,7 +50,10 @@ export default class ProjectCard extends React.PureComponent {
return null;
}

const areProjectMeasuresLoaded = this.props.measures != null;
// check reliability_rating because only some measures can be loaded
// if coming from visualizations tab
const areProjectMeasuresLoaded = this.props.measures != null &&
this.props.measures['reliability_rating'] != null;
const isProjectAnalyzed = project.analysisDate != null;
const displayQualityGate = areProjectMeasuresLoaded && isProjectAnalyzed;


+ 50
- 0
server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js Visa fil

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import RadioToggle from '../../../components/controls/RadioToggle';
import { translate } from '../../../helpers/l10n';

export default class ViewSelect extends React.PureComponent {
props: {
onChange: (string) => void,
view: string
};

handleChange = (view: string) => {
this.props.onChange(view);
};

render() {
const options = ['list', 'visualizations'].map(option => ({
value: option,
label: translate('projects.view', option)
}));

return (
<RadioToggle
name="view"
onCheck={this.handleChange}
options={options}
value={this.props.view}
/>
);
}
}

+ 33
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js Visa fil

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { shallow } from 'enzyme';
import PageSidebar from '../PageSidebar';

it('should handle `view` and `visualization`', () => {
const query = {
view: 'visualizations',
visualization: 'bugs'
};
const sidebar = shallow(<PageSidebar query={query} isFavorite={false} />);
expect(sidebar.find('.projects-facets-reset')).toMatchSnapshot();
sidebar.setProps({ query: { ...query, size: '3' } });
expect(sidebar.find('.projects-facets-reset')).toMatchSnapshot();
});

+ 26
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js Visa fil

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { shallow } from 'enzyme';
import ViewSelect from '../ViewSelect';

it('should render options', () => {
expect(shallow(<ViewSelect view="visualizations" />)).toMatchSnapshot();
});

+ 22
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap Visa fil

@@ -0,0 +1,22 @@
exports[`test should handle \`view\` and \`visualization\` 1`] = `undefined`;

exports[`test should handle \`view\` and \`visualization\` 2`] = `
<div
className="projects-facets-reset">
<Link
className="button button-red"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects",
"query": Object {
"view": "visualizations",
"visualization": "bugs",
},
}
}>
projects.clear_all_filters
</Link>
</div>
`;

+ 19
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap Visa fil

@@ -0,0 +1,19 @@
exports[`test should render options 1`] = `
<RadioToggle
disabled={false}
name="view"
onCheck={[Function]}
options={
Array [
Object {
"label": "projects.view.list",
"value": "list",
},
Object {
"label": "projects.view.visualizations",
"value": "visualizations",
},
]
}
value="visualizations" />
`;

+ 30
- 9
server/sonar-web/src/main/js/apps/projects/store/actions.js Visa fil

@@ -34,6 +34,7 @@ import { getOrganizations } from '../../../api/organizations';
import { receiveOrganizations } from '../../../store/organizations/duck';

const PAGE_SIZE = 50;
const PAGE_SIZE_VISUALIZATIONS = 99;

const METRICS = [
'alert_status',
@@ -46,6 +47,16 @@ const METRICS = [
'ncloc_language_distribution'
];

const METRICS_BY_VISUALIZATION = {
quality: ['reliability_rating', 'security_rating', 'coverage', 'ncloc', 'sqale_index'],
// x, y, size, color
bugs: ['ncloc', 'reliability_remediation_effort', 'bugs', 'reliability_rating'],
vulnerabilities: ['ncloc', 'security_remediation_effort', 'vulnerabilities', 'security_rating'],
code_smells: ['ncloc', 'sqale_index', 'code_smells', 'sqale_rating'],
uncovered_lines: ['complexity', 'coverage', 'uncovered_lines'],
duplicated_blocks: ['ncloc', 'duplicated_lines', 'duplicated_blocks']
};

const FACETS = [
'reliability_rating',
'security_rating',
@@ -90,14 +101,23 @@ const onReceiveOrganizations = dispatch =>
dispatch(receiveOrganizations(response.organizations));
};

const fetchProjectMeasures = projects =>
const defineMetrics = query => {
if (query.view === 'visualizations') {
return METRICS_BY_VISUALIZATION[query.visualization || 'quality'];
} else {
return METRICS;
}
};

const fetchProjectMeasures = (projects, query) =>
dispatch => {
if (!projects.length) {
return Promise.resolve();
}

const projectKeys = projects.map(project => project.key);
return getMeasuresForProjects(projectKeys, METRICS).then(
const metrics = defineMetrics(query);
return getMeasuresForProjects(projectKeys, metrics).then(
onReceiveMeasures(dispatch, projectKeys),
onFail(dispatch)
);
@@ -124,13 +144,13 @@ const handleFavorites = (dispatch, projects) => {
}
};

const onReceiveProjects = dispatch =>
const onReceiveProjects = (dispatch, query) =>
response => {
dispatch(receiveComponents(response.components));
dispatch(receiveProjects(response.components, response.facets));
handleFavorites(dispatch, response.components);
Promise.all([
dispatch(fetchProjectMeasures(response.components)),
dispatch(fetchProjectMeasures(response.components, query)),
dispatch(fetchProjectOrganizations(response.components))
]).then(() => {
dispatch(updateState({ loading: false }));
@@ -143,13 +163,13 @@ const onReceiveProjects = dispatch =>
);
};

const onReceiveMoreProjects = dispatch =>
const onReceiveMoreProjects = (dispatch, query) =>
response => {
dispatch(receiveComponents(response.components));
dispatch(receiveMoreProjects(response.components));
handleFavorites(dispatch, response.components);
Promise.all([
dispatch(fetchProjectMeasures(response.components)),
dispatch(fetchProjectMeasures(response.components, query)),
dispatch(fetchProjectOrganizations(response.components))
]).then(() => {
dispatch(updateState({ loading: false }));
@@ -160,12 +180,13 @@ const onReceiveMoreProjects = dispatch =>
export const fetchProjects = (query, isFavorite, organization) =>
dispatch => {
dispatch(updateState({ loading: true }));
const ps = query.view === 'visualizations' ? PAGE_SIZE_VISUALIZATIONS : PAGE_SIZE;
const data = convertToQueryData(query, isFavorite, organization, {
ps: PAGE_SIZE,
ps,
facets: FACETS.join(),
f: 'analysisDate'
});
return searchProjects(data).then(onReceiveProjects(dispatch), onFail(dispatch));
return searchProjects(data).then(onReceiveProjects(dispatch, query), onFail(dispatch));
};

export const fetchMoreProjects = (query, isFavorite, organization) =>
@@ -178,7 +199,7 @@ export const fetchMoreProjects = (query, isFavorite, organization) =>
p: pageIndex + 1,
f: 'analysisDate'
});
return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch));
return searchProjects(data).then(onReceiveMoreProjects(dispatch, query), onFail(dispatch));
};

export const setProjectTags = (project, tags) =>

+ 11
- 1
server/sonar-web/src/main/js/apps/projects/store/utils.js Visa fil

@@ -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 { VISUALIZATIONS } from '../utils';

const getAsNumericRating = value => {
if (value === '' || value == null || isNaN(value)) {
return null;
@@ -46,6 +48,12 @@ const getAsArray = (values, elementGetter) => {
return values.split(',').map(elementGetter);
};

const getView = rawValue => rawValue === 'visualizations' ? rawValue : undefined;

const getVisualization = value => {
return VISUALIZATIONS.includes(value) ? value : null;
};

export const parseUrlQuery = urlQuery => ({
gate: getAsLevel(urlQuery['gate']),
reliability: getAsNumericRating(urlQuery['reliability']),
@@ -57,7 +65,9 @@ export const parseUrlQuery = urlQuery => ({
languages: getAsArray(urlQuery['languages'], getAsString),
tags: getAsArray(urlQuery['tags'], getAsString),
search: getAsString(urlQuery['search']),
sort: getAsString(urlQuery['sort'])
sort: getAsString(urlQuery['sort']),
view: getView(urlQuery['view']),
visualization: getVisualization(urlQuery['visualization'])
});

export const mapMetricToProperty = metricKey => {

+ 22
- 0
server/sonar-web/src/main/js/apps/projects/styles.css Visa fil

@@ -181,3 +181,25 @@
.search-navigator-facet-highlight-under.active ~ .search-navigator-facet .projects-facet-bar-inner {
background-color: #4b9fd5;
}

.projects-visualization {
position: relative;
height: 600px;
margin-top: 15px;
border-top: 1px solid #e6e6e6;
}

.projects-visualization .measure-details-bubble-chart-axis.y {
width: 300px;
left: 15px;
margin-top: 150px;
transform-origin: 0 0;
text-align: center;
}

.projects-visualizations-footer {
padding: 15px 0;
color: #777;
font-size: 12px;
text-align: center;
}

+ 15
- 0
server/sonar-web/src/main/js/apps/projects/utils.js Visa fil

@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import { translate } from '../../helpers/l10n';

const LOCALSTORAGE_KEY = 'sonarqube.projects.default';
const LOCALSTORAGE_FAVORITE = 'favorite';
const LOCALSTORAGE_ALL = 'all';
@@ -44,3 +46,16 @@ const save = (value: string) => {
export const saveAll = () => save(LOCALSTORAGE_ALL);

export const saveFavorite = () => save(LOCALSTORAGE_FAVORITE);

export const VISUALIZATIONS = [
'quality',
'bugs',
'vulnerabilities',
'code_smells',
'uncovered_lines',
'duplicated_blocks'
];

export const localizeSorting = (sort?: string) => {
return translate('projects.sort', sort || 'name');
};

+ 36
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js Visa fil

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SimpleBubbleChart from './SimpleBubbleChart';

export default class Bugs extends React.PureComponent {
render() {
return (
<SimpleBubbleChart
{...this.props}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'reliability_remediation_effort', type: 'SHORT_WORK_DUR' }}
sizeMetric={{ key: 'bugs', type: 'SHORT_INT' }}
colorMetric="reliability_rating"
/>
);
}
}

+ 36
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js Visa fil

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SimpleBubbleChart from './SimpleBubbleChart';

export default class CodeSmells extends React.PureComponent {
render() {
return (
<SimpleBubbleChart
{...this.props}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'sqale_index', type: 'SHORT_WORK_DUR' }}
sizeMetric={{ key: 'code_smells', type: 'SHORT_INT' }}
colorMetric="sqale_rating"
/>
);
}
}

+ 35
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js Visa fil

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SimpleBubbleChart from './SimpleBubbleChart';

export default class DuplicatedBlocks extends React.PureComponent {
render() {
return (
<SimpleBubbleChart
{...this.props}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'duplicated_lines', type: 'SHORT_INT' }}
sizeMetric={{ key: 'duplicated_blocks', type: 'SHORT_INT' }}
/>
);
}
}

+ 133
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js Visa fil

@@ -0,0 +1,133 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubbleChart from '../../../components/charts/BubbleChart';
import { formatMeasure } from '../../../helpers/measures';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { RATING_COLORS } from '../../../helpers/constants';

type Project = {
key: string,
measures: { [string]: string },
name: string,
organization?: { name: string }
};

const X_METRIC = 'sqale_index';
const X_METRIC_TYPE = 'SHORT_WORK_DUR';
const Y_METRIC = 'coverage';
const Y_METRIC_TYPE = 'PERCENT';
const SIZE_METRIC = 'ncloc';
const SIZE_METRIC_TYPE = 'SHORT_INT';
const COLOR_METRIC_1 = 'reliability_rating';
const COLOR_METRIC_2 = 'security_rating';
const COLOR_METRIC_TYPE = 'RATING';

export default class QualityModel extends React.PureComponent {
props: {
onProjectOpen: (?string) => void,
projects: Array<Project>
};

getMetricTooltip(metric: { key: string, type: string }, value: number) {
const name = translate('metric', metric.key, 'name');
return `<div>${name}: ${formatMeasure(value, metric.type)}</div>`;
}

getTooltip(project: Project, x: number, y: number, size: number, color1: number, color2: number) {
const fullProjectName = project.organization
? `<div class="little-spacer-bottom">${project.organization.name} / <strong>${project.name}</strong></div>`
: `<div class="little-spacer-bottom"><strong>${project.name}</strong></div>`;
const inner = [
fullProjectName,
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),
this.getMetricTooltip({ key: X_METRIC, type: X_METRIC_TYPE }, x),
this.getMetricTooltip({ key: SIZE_METRIC, type: SIZE_METRIC_TYPE }, size)
].join('');

return `<div class="text-left">${inner}</div>`;
}

render() {
const items = this.props.projects
.filter(
({ measures }) =>
measures[X_METRIC] != null &&
measures[Y_METRIC] != null &&
measures[SIZE_METRIC] != null &&
measures[COLOR_METRIC_1] != null &&
measures[COLOR_METRIC_2] != null
)
.map(project => {
const x = Number(project.measures[X_METRIC]);
const y = Number(project.measures[Y_METRIC]);
const size = Number(project.measures[SIZE_METRIC]);
const color1 = Number(project.measures[COLOR_METRIC_1]);
const color2 = Number(project.measures[COLOR_METRIC_2]);
return {
x,
y,
size,
color: RATING_COLORS[Math.max(color1, color2) - 1],
key: project.key,
tooltip: this.getTooltip(project, x, y, size, color1, color2),
link: project.key
};
});

const formatXTick = tick => formatMeasure(tick, X_METRIC_TYPE);
const formatYTick = tick => formatMeasure(tick, Y_METRIC_TYPE);

return (
<div>
<BubbleChart
formatXTick={formatXTick}
formatYTick={formatYTick}
height={600}
items={items}
padding={[40, 20, 60, 100]}
onBubbleClick={this.props.onProjectOpen}
yDomain={[100, 0]}
/>
<div className="measure-details-bubble-chart-axis x">
{translate('metric', X_METRIC, 'name')}
</div>
<div className="measure-details-bubble-chart-axis y">
{translate('metric', Y_METRIC, 'name')}
</div>
<div className="measure-details-bubble-chart-axis size">
<span className="spacer-right">
{translateWithParameters(
'component_measures.legend.color_x',
translate('projects.worse_of_reliablity_and_security')
)}
</span>
{translateWithParameters(
'component_measures.legend.size_x',
translate('metric', SIZE_METRIC, 'name')
)}
</div>
</div>
);
}
}

+ 125
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js Visa fil

@@ -0,0 +1,125 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubbleChart from '../../../components/charts/BubbleChart';
import { formatMeasure } from '../../../helpers/measures';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { RATING_COLORS } from '../../../helpers/constants';

type Metric = { key: string, type: string };

type Project = {
key: string,
measures: { [string]: string },
name: string,
organization?: { name: string }
};

export default class SimpleBubbleChart extends React.PureComponent {
props: {
onProjectOpen: (?string) => void,
projects: Array<Project>,
sizeMetric: Metric,
xMetric: Metric,
yDomain?: [number, number],
yMetric: Metric,
colorMetric?: string
};

getMetricTooltip(metric: Metric, value: number) {
const name = translate('metric', metric.key, 'name');
return `<div>${name}: ${formatMeasure(value, metric.type)}</div>`;
}

getTooltip(project: Project, x: number, y: number, size: number, color?: number) {
const fullProjectName = project.organization
? `<div class="little-spacer-bottom">${project.organization.name} / <strong>${project.name}</strong></div>`
: `<div class="little-spacer-bottom"><strong>${project.name}</strong></div>`;

const inner = [
fullProjectName,
this.getMetricTooltip(this.props.xMetric, x),
this.getMetricTooltip(this.props.yMetric, y),
this.getMetricTooltip(this.props.sizeMetric, size)
];

if (color) {
// $FlowFixMe if `color` is defined then `this.props.colorMetric` is defined too
this.getMetricTooltip({ key: this.props.colorMetric, type: 'RATING' }, color);
}

return `<div class="text-left">${inner.join('')}</div>`;
}

render() {
const { xMetric, yMetric, sizeMetric, colorMetric } = this.props;

const items = this.props.projects
.filter(project => project.measures[xMetric.key] != null)
.filter(project => project.measures[yMetric.key] != null)
.filter(project => project.measures[sizeMetric.key] != null)
.filter(project => colorMetric == null || project.measures[colorMetric] !== null)
.map(project => {
const x = Number(project.measures[xMetric.key]);
const y = Number(project.measures[yMetric.key]);
const size = Number(project.measures[sizeMetric.key]);
const color = colorMetric ? Number(project.measures[colorMetric]) : undefined;
return {
x,
y,
size,
color: color ? RATING_COLORS[color - 1] : undefined,
key: project.key,
tooltip: this.getTooltip(project, x, y, size, color),
link: project.key
};
});

const formatXTick = tick => formatMeasure(tick, xMetric.type);
const formatYTick = tick => formatMeasure(tick, yMetric.type);

return (
<div>
<BubbleChart
formatXTick={formatXTick}
formatYTick={formatYTick}
height={600}
items={items}
onBubbleClick={this.props.onProjectOpen}
padding={[40, 20, 60, 100]}
yDomain={this.props.yDomain}
/>
<div className="measure-details-bubble-chart-axis x">
{translate('metric', xMetric.key, 'name')}
</div>
<div className="measure-details-bubble-chart-axis y">
{translate('metric', yMetric.key, 'name')}
</div>
<div className="measure-details-bubble-chart-axis size">
{translateWithParameters(
'component_measures.legend.size_x',
translate('metric', sizeMetric.key, 'name')
)}
</div>
</div>
);
}
}

+ 36
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js Visa fil

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SimpleBubbleChart from './SimpleBubbleChart';

export default class UncoveredLines extends React.PureComponent {
render() {
return (
<SimpleBubbleChart
{...this.props}
xMetric={{ key: 'complexity', type: 'SHORT_INT' }}
yMetric={{ key: 'coverage', type: 'PERCENT' }}
yDomain={[100, 0]}
sizeMetric={{ key: 'uncovered_lines', type: 'SHORT_INT' }}
/>
);
}
}

+ 94
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js Visa fil

@@ -0,0 +1,94 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import VisualizationsHeader from './VisualizationsHeader';
import QualityModel from './QualityModel';
import Bugs from './Bugs';
import Vulnerabilities from './Vulnerabilities';
import CodeSmells from './CodeSmells';
import UncoveredLines from './UncoveredLines';
import DuplicatedBlocks from './DuplicatedBlocks';
import { localizeSorting } from '../utils';
import { translateWithParameters } from '../../../helpers/l10n';

export default class Visualizations extends React.PureComponent {
props: {
onProjectOpen: (string) => void,
onVisualizationChange: (string) => void,
projects?: Array<*>,
sort?: string,
total?: number,
visualization: string
};

renderVisualization(projects: Array<*>) {
const visualizationToComponent = {
quality: QualityModel,
bugs: Bugs,
vulnerabilities: Vulnerabilities,
code_smells: CodeSmells,
uncovered_lines: UncoveredLines,
duplicated_blocks: DuplicatedBlocks
};
const Component = visualizationToComponent[this.props.visualization];

return Component
? <Component onProjectOpen={this.props.onProjectOpen} projects={projects} />
: null;
}

renderFooter() {
const { projects, total, sort } = this.props;

if (projects == null || total == null || projects.length >= total) {
return null;
}

return (
<footer className="projects-visualizations-footer">
{translateWithParameters(
'projects.limited_set_of_projects',
projects.length,
localizeSorting(sort)
)}
</footer>
);
}

render() {
const { projects } = this.props;

return (
<div className="boxed-group projects-visualizations">
<VisualizationsHeader
onVisualizationChange={this.props.onVisualizationChange}
visualization={this.props.visualization}
/>
<div className="projects-visualization">
<div>
{projects != null && this.renderVisualization(projects)}
</div>
</div>
{this.renderFooter()}
</div>
);
}
}

+ 44
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js Visa fil

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
import Visualizations from './Visualizations';
import {
getProjects,
getComponent,
getComponentMeasures,
getOrganizationByKey,
getProjectsAppState
} from '../../../store/rootReducer';

const mapStateToProps = state => {
const projectKeys = getProjects(state) || [];
const projects = projectKeys.map(key => {
const component = getComponent(state, key);
return {
...component,
measures: getComponentMeasures(state, key) || {},
organization: getOrganizationByKey(state, component.organization)
};
});
const appState = getProjectsAppState(state);
return { projects, total: appState.total };
};

export default connect(mapStateToProps)(Visualizations);

+ 57
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js Visa fil

@@ -0,0 +1,57 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Select from 'react-select';
import { translate } from '../../../helpers/l10n';
import { VISUALIZATIONS } from '../utils';

export default class VisualizationsHeader extends React.PureComponent {
props: {
onVisualizationChange: (string) => void,
visualization: string
};

handleChange = (option: { value: string }) => {
this.props.onVisualizationChange(option.value);
};

render() {
const options = VISUALIZATIONS.map(option => ({
value: option,
label: option === 'quality'
? translate('projects.quality_model')
: translate('metric', option, 'name')
}));

return (
<header className="boxed-group-header">
<Select
className="input-medium"
clearable={false}
onChange={this.handleChange}
options={options}
searchable={false}
value={this.props.visualization}
/>
</header>
);
}
}

+ 36
- 0
server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js Visa fil

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SimpleBubbleChart from './SimpleBubbleChart';

export default class Vulnerabilities extends React.PureComponent {
render() {
return (
<SimpleBubbleChart
{...this.props}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'security_remediation_effort', type: 'SHORT_WORK_DUR' }}
sizeMetric={{ key: 'vulnerabilities', type: 'SHORT_INT' }}
colorMetric="security_rating"
/>
);
}
}

server/sonar-web/src/main/js/components/charts/bubble-chart.js → server/sonar-web/src/main/js/components/charts/BubbleChart.js Visa fil

@@ -17,108 +17,127 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import d3 from 'd3';
// @flow
import React from 'react';
import d3 from 'd3';
import sortBy from 'lodash/sortBy';
import uniq from 'lodash/uniq';
import { ResizeMixin } from './../mixins/resize-mixin';
import { TooltipsMixin } from './../mixins/tooltips-mixin';
import { AutoSizer } from 'react-virtualized';
import { TooltipsContainer } from '../mixins/tooltips-mixin';

type Scale = {
(number): number,
range: () => [number, number],
ticks: (number) => Array<number>
};

const TICKS_COUNT = 5;

export const Bubble = React.createClass({
propTypes: {
x: React.PropTypes.number.isRequired,
y: React.PropTypes.number.isRequired,
r: React.PropTypes.number.isRequired,
tooltip: React.PropTypes.string,
link: React.PropTypes.any
},
class Bubble extends React.PureComponent {
props: {
color?: string,
link?: string,
onClick: (?string) => void,
r: number,
tooltip?: string,
x: number,
y: number
};

handleClick() {
handleClick = () => {
if (this.props.onClick) {
this.props.onClick(this.props.link);
}
},
};

render() {
let tooltipAttrs = {};
if (this.props.tooltip) {
tooltipAttrs = {
'data-toggle': 'tooltip',
title: this.props.tooltip
};
}
return (
<circle
onClick={this.handleClick}
className="bubble-chart-bubble"
r={this.props.r}
{...tooltipAttrs}
transform={`translate(${this.props.x}, ${this.props.y})`}
/>
const tooltipAttrs = this.props.tooltip
? {
'data-toggle': 'tooltip',
title: this.props.tooltip
}
: {};

const circle = (
<g>
<circle
{...tooltipAttrs}
onClick={this.handleClick}
className="bubble-chart-bubble"
r={this.props.r}
style={{
fill: this.props.color,
stroke: this.props.color
}}
transform={`translate(${this.props.x}, ${this.props.y})`}
/>
</g>
);

return this.props.tooltip ? <TooltipsContainer>{circle}</TooltipsContainer> : circle;
}
});

export const BubbleChart = React.createClass({
propTypes: {
items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
displayXGrid: React.PropTypes.bool,
displayXTicks: React.PropTypes.bool,
displayYGrid: React.PropTypes.bool,
displayYTicks: React.PropTypes.bool,
height: React.PropTypes.number,
padding: React.PropTypes.arrayOf(React.PropTypes.number),
formatXTick: React.PropTypes.func,
formatYTick: React.PropTypes.func,
onBubbleClick: React.PropTypes.func
},

mixins: [ResizeMixin, TooltipsMixin],

getDefaultProps() {
return {
sizeRange: [5, 45],
displayXGrid: true,
displayYGrid: true,
displayXTicks: true,
displayYTicks: true,
padding: [10, 10, 10, 10],
formatXTick: d => d,
formatYTick: d => d
};
},

getInitialState() {
return { width: this.props.width, height: this.props.height };
},

getXRange(xScale, sizeScale, availableWidth) {
}

export default class BubbleChart extends React.PureComponent {
props: {|
items: Array<{|
x: number,
y: number,
size: number,
color?: string,
key?: string,
link?: string,
tooltip?: string
|}>,
sizeRange?: [number, number],
displayXGrid: boolean,
displayXTicks: boolean,
displayYGrid: boolean,
displayYTicks: boolean,
height: number,
padding: [number, number, number, number],
formatXTick: (number) => string,
formatYTick: (number) => string,
onBubbleClick?: (?string) => void,
xDomain?: [number, number],
yDomain?: [number, number]
|};

static defaultProps = {
sizeRange: [5, 45],
displayXGrid: true,
displayYGrid: true,
displayXTicks: true,
displayYTicks: true,
padding: [10, 10, 10, 10],
formatXTick: d => d,
formatYTick: d => d
};

getXRange(xScale: Scale, sizeScale: Scale, availableWidth: number) {
const minX = d3.min(this.props.items, d => xScale(d.x) - sizeScale(d.size));
const maxX = d3.max(this.props.items, d => xScale(d.x) + sizeScale(d.size));
const dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0];
const dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
return [dMinX, availableWidth - dMaxX];
},
}

getYRange(yScale, sizeScale, availableHeight) {
getYRange(yScale: Scale, sizeScale: Scale, availableHeight: number) {
const minY = d3.min(this.props.items, d => yScale(d.y) - sizeScale(d.size));
const maxY = d3.max(this.props.items, d => yScale(d.y) + sizeScale(d.size));
const dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1];
const dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
return [availableHeight - dMaxY, dMinY];
},
}

getTicks(scale, format) {
getTicks(scale: Scale, format: (number) => string) {
const ticks = scale.ticks(TICKS_COUNT).map(tick => format(tick));
const uniqueTicksCount = uniq(ticks).length;
const ticksCount = uniqueTicksCount < TICKS_COUNT ? uniqueTicksCount - 1 : TICKS_COUNT;
return scale.ticks(ticksCount);
},
}

renderXGrid(ticks, xScale, yScale) {
renderXGrid(ticks: Array<number>, xScale: Scale, yScale: Scale) {
if (!this.props.displayXGrid) {
return null;
}
@@ -131,9 +150,9 @@ export const BubbleChart = React.createClass({
});

return <g ref="xGrid">{lines}</g>;
},
}

renderYGrid(ticks, xScale, yScale) {
renderYGrid(ticks: Array<number>, xScale: Scale, yScale: Scale) {
if (!this.props.displayYGrid) {
return null;
}
@@ -146,9 +165,9 @@ export const BubbleChart = React.createClass({
});

return <g ref="yGrid">{lines}</g>;
},
}

renderXTicks(xTicks, xScale, yScale) {
renderXTicks(xTicks: Array<number>, xScale: Scale, yScale: Scale) {
if (!this.props.displayXTicks) {
return null;
}
@@ -165,9 +184,9 @@ export const BubbleChart = React.createClass({
});

return <g>{ticks}</g>;
},
}

renderYTicks(yTicks, xScale, yScale) {
renderYTicks(yTicks: Array<number>, xScale: Scale, yScale: Scale) {
if (!this.props.displayYTicks) {
return null;
}
@@ -183,37 +202,32 @@ export const BubbleChart = React.createClass({
x={x}
y={y}
dx="-0.5em"
dy="0.3em"
>
dy="0.3em">
{innerText}
</text>
);
});

return <g>{ticks}</g>;
},

render() {
if (!this.state.width || !this.state.height) {
return <div />;
}
}

const availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
const availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
renderChart(width: number) {
const availableWidth = width - this.props.padding[1] - this.props.padding[3];
const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];

const xScale = d3.scale
.linear()
.domain([0, d3.max(this.props.items, d => d.x)])
.domain(this.props.xDomain || [0, d3.max(this.props.items, d => d.x)])
.range([0, availableWidth])
.nice();
const yScale = d3.scale
.linear()
.domain([0, d3.max(this.props.items, d => d.y)])
.domain(this.props.yDomain || [0, d3.max(this.props.items, d => d.y)])
.range([availableHeight, 0])
.nice();
const sizeScale = d3.scale
.linear()
.domain([0, d3.max(this.props.items, d => d.size)])
.domain(this.props.sizeDomain || [0, d3.max(this.props.items, d => d.size)])
.range(this.props.sizeRange);

const xScaleOriginal = xScale.copy();
@@ -225,12 +239,13 @@ export const BubbleChart = React.createClass({
const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
return (
<Bubble
key={index}
key={item.key || index}
link={item.link}
tooltip={item.tooltip}
x={xScale(item.x)}
y={yScale(item.y)}
r={sizeScale(item.size)}
color={item.color}
onClick={this.props.onBubbleClick}
/>
);
@@ -240,7 +255,7 @@ export const BubbleChart = React.createClass({
const yTicks = this.getTicks(yScale, this.props.formatYTick);

return (
<svg className="bubble-chart" width={this.state.width} height={this.state.height}>
<svg className="bubble-chart" width={width} height={this.props.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
{this.renderXGrid(xTicks, xScale, yScale)}
{this.renderXTicks(xTicks, xScale, yScaleOriginal)}
@@ -251,4 +266,12 @@ export const BubbleChart = React.createClass({
</svg>
);
}
});

render() {
return (
<AutoSizer disableHeight={true}>
{size => this.renderChart(size.width)}
</AutoSizer>
);
}
}

server/sonar-web/src/main/js/components/charts/__tests__/bubble-chart-test.js → server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.js Visa fil

@@ -18,23 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { BubbleChart, Bubble } from '../bubble-chart';
import { mount } from 'enzyme';
import BubbleChart from '../BubbleChart';

it('should display bubbles', () => {
const items = [{ x: 1, y: 10, size: 7 }, { x: 2, y: 30, size: 5 }, { x: 3, y: 20, size: 2 }];
const chart = shallow(<BubbleChart items={items} width={100} height={100} />);
expect(chart.find(Bubble).length).toBe(3);
});

it('should display grid', () => {
const items = [{ x: 1, y: 10, size: 7 }, { x: 2, y: 30, size: 5 }, { x: 3, y: 20, size: 2 }];
const chart = shallow(<BubbleChart items={items} width={100} height={100} />);
expect(chart.find('line').length).toBeGreaterThan(0);
});

it('should display ticks', () => {
const items = [{ x: 1, y: 10, size: 7 }, { x: 2, y: 30, size: 5 }, { x: 3, y: 20, size: 2 }];
const chart = shallow(<BubbleChart items={items} width={100} height={100} />);
expect(chart.find('.bubble-chart-tick').length).toBeGreaterThan(0);
const chart = mount(<BubbleChart items={items} height={100} />);
expect(chart).toMatchSnapshot();
});

+ 311
- 0
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.js.snap Visa fil

@@ -0,0 +1,311 @@
exports[`test should display bubbles 1`] = `
<BubbleChart
displayXGrid={true}
displayXTicks={true}
displayYGrid={true}
displayYTicks={true}
formatXTick={[Function]}
formatYTick={[Function]}
height={100}
items={
Array [
Object {
"size": 7,
"x": 1,
"y": 10,
},
Object {
"size": 5,
"x": 2,
"y": 30,
},
Object {
"size": 2,
"x": 3,
"y": 20,
},
]
}
padding={
Array [
10,
10,
10,
10,
]
}
sizeRange={
Array [
5,
45,
]
}>
<AutoSizer
disableHeight={true}
onResize={[Function]}>
<div
style={
Object {
"overflow": "visible",
"width": 0,
}
}>
<svg
className="bubble-chart"
height={100}
width={0}>
<g
transform="translate(10, 10)">
<g>
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={51.666666666666664}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={30}
x2={30}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={8.333333333333336}
x2={8.333333333333336}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={-13.33333333333334}
x2={-13.33333333333334}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={-35}
x2={-35}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={-56.66666666666668}
x2={-56.66666666666668}
y1={61.66666666666666}
y2={33.57142857142858} />
<line
className="bubble-chart-grid"
x1={-78.33333333333334}
x2={-78.33333333333334}
y1={61.66666666666666}
y2={33.57142857142858} />
</g>
<g>
<text
className="bubble-chart-tick"
dy="1.5em"
x={51.666666666666664}
y={80} />
<text
className="bubble-chart-tick"
dy="1.5em"
x={30}
y={80}>
0.5
</text>
<text
className="bubble-chart-tick"
dy="1.5em"
x={8.333333333333336}
y={80}>
1
</text>
<text
className="bubble-chart-tick"
dy="1.5em"
x={-13.33333333333334}
y={80}>
1.5
</text>
<text
className="bubble-chart-tick"
dy="1.5em"
x={-35}
y={80}>
2
</text>
<text
className="bubble-chart-tick"
dy="1.5em"
x={-56.66666666666668}
y={80}>
2.5
</text>
<text
className="bubble-chart-tick"
dy="1.5em"
x={-78.33333333333334}
y={80}>
3
</text>
</g>
<g>
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={61.66666666666666}
y2={61.66666666666666} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={56.98412698412698}
y2={56.98412698412698} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={52.3015873015873}
y2={52.3015873015873} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={47.61904761904762}
y2={47.61904761904762} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={42.93650793650794}
y2={42.93650793650794} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={38.25396825396825}
y2={38.25396825396825} />
<line
className="bubble-chart-grid"
x1={51.666666666666664}
x2={-78.33333333333334}
y1={33.57142857142858}
y2={33.57142857142858} />
</g>
<g>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={61.66666666666666} />
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={56.98412698412698}>
5
</text>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={52.3015873015873}>
10
</text>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={47.61904761904762}>
15
</text>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={42.93650793650794}>
20
</text>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={38.25396825396825}>
25
</text>
<text
className="bubble-chart-tick bubble-chart-tick-y"
dx="-0.5em"
dy="0.3em"
x={0}
y={33.57142857142858}>
30
</text>
</g>
<Bubble
r={45}
x={8.333333333333336}
y={52.3015873015873}>
<g>
<circle
className="bubble-chart-bubble"
onClick={[Function]}
r={45}
style={
Object {
"fill": undefined,
"stroke": undefined,
}
}
transform="translate(8.333333333333336, 52.3015873015873)" />
</g>
</Bubble>
<Bubble
r={33.57142857142858}
x={-35}
y={33.57142857142858}>
<g>
<circle
className="bubble-chart-bubble"
onClick={[Function]}
r={33.57142857142858}
style={
Object {
"fill": undefined,
"stroke": undefined,
}
}
transform="translate(-35, 33.57142857142858)" />
</g>
</Bubble>
<Bubble
r={16.428571428571427}
x={-78.33333333333334}
y={42.93650793650794}>
<g>
<circle
className="bubble-chart-bubble"
onClick={[Function]}
r={16.428571428571427}
style={
Object {
"fill": undefined,
"stroke": undefined,
}
}
transform="translate(-78.33333333333334, 42.93650793650794)" />
</g>
</Bubble>
</g>
</svg>
</div>
</AutoSizer>
</BubbleChart>
`;

+ 2
- 0
server/sonar-web/src/main/js/helpers/constants.js Visa fil

@@ -28,3 +28,5 @@ export const CHART_REVERSED_COLORS_RANGE_PERCENT = [
'#b0d513',
'#00aa00'
];

export const RATING_COLORS = ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#e00'];

+ 0
- 1
server/sonar-web/src/main/less/components/react-select.less Visa fil

@@ -88,7 +88,6 @@
.Select-placeholder,
:not(.Select--multi) > .Select-control .Select-value {
bottom: 0;
color: #aaa;
left: 0;
line-height: @formControlHeight - 1px;
padding-left: 8px;

+ 18
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

@@ -833,6 +833,24 @@ projects.explore_projects=Explore Projects
projects.not_analyzed=Project is not analyzed yet.
projects.search=Search by project name or key
projects.sort_list=Sort list by
projects.view.list=List
projects.view.visualizations=Visualizations
projects.quality_model=Quality Model
projects.worse_of_reliablity_and_security=Worse of Reliability and Security
projects.limited_set_of_projects=Displayed project set limited to the top {0} projects based on current sort: {1}.
projects.sort.name=by name
projects.sort.reliability=by reliability (best first)
projects.sort.-reliability=by reliability (worst first)
projects.sort.security=by security (best first)
projects.sort.-security=by security (worst first)
projects.sort.maintainability=by maintainability (best first)
projects.sort.-maintainability=by maintainability (worst first)
projects.sort.coverage=by coverage (best first)
projects.sort.-coverage=by coverage (worst first)
projects.sort.duplications=by duplications (best first)
projects.sort.-duplications=by duplications (worst first)
projects.sort.size=by size (smallest first)
projects.sort.-size=by size (biggest first)


#------------------------------------------------------------------------------

Laddar…
Avbryt
Spara