"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,
*/
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';
.measure-details-bubble-chart-axis.x {
left: 50%;
bottom: 10px;
+ width: 500px;
+ margin-left: -250px;
+ text-align: center;
}
.measure-details-bubble-chart-axis.y {
.measure-details-bubble-chart-axis.size {
left: 50%;
top: 10px;
+ width: 500px;
+ margin-left: -250px;
+ text-align: center;
}
.measure-details-treemap {
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 = {
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;
<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>
);
* 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));
* 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';
isFavorite: true
});
-export default connect(mapStateToProps, { fetchProjects })(AllProjects);
+export default connect(mapStateToProps, { fetchProjects })(withRouter(AllProjects));
* 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>
};
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">
<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}
/>
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;
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+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>
+`;
--- /dev/null
+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" />
+`;
import { receiveOrganizations } from '../../../store/organizations/duck';
const PAGE_SIZE = 50;
+const PAGE_SIZE_VISUALIZATIONS = 99;
const METRICS = [
'alert_status',
'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',
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)
);
}
};
-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 }));
);
};
-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 }));
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) =>
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) =>
* 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;
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']),
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 => {
.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;
+}
\ No newline at end of file
* 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';
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');
+};
--- /dev/null
+/*
+ * 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"
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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"
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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' }}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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' }}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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);
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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"
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 d3 from 'd3';
+import sortBy from 'lodash/sortBy';
+import uniq from 'lodash/uniq';
+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;
+
+class Bubble extends React.PureComponent {
+ props: {
+ color?: string,
+ link?: string,
+ onClick: (?string) => void,
+ r: number,
+ tooltip?: string,
+ x: number,
+ y: number
+ };
+
+ handleClick = () => {
+ if (this.props.onClick) {
+ this.props.onClick(this.props.link);
+ }
+ };
+
+ render() {
+ 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 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: 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: 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: Array<number>, xScale: Scale, yScale: Scale) {
+ if (!this.props.displayXGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const x = xScale(tick);
+ const y1 = yScale.range()[0];
+ const y2 = yScale.range()[1];
+ return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid" />;
+ });
+
+ return <g ref="xGrid">{lines}</g>;
+ }
+
+ renderYGrid(ticks: Array<number>, xScale: Scale, yScale: Scale) {
+ if (!this.props.displayYGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const y = yScale(tick);
+ const x1 = xScale.range()[0];
+ const x2 = xScale.range()[1];
+ return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid" />;
+ });
+
+ return <g ref="yGrid">{lines}</g>;
+ }
+
+ renderXTicks(xTicks: Array<number>, xScale: Scale, yScale: Scale) {
+ if (!this.props.displayXTicks) {
+ return null;
+ }
+
+ const ticks = xTicks.map((tick, index) => {
+ const x = xScale(tick);
+ const y = yScale.range()[0];
+ const innerText = this.props.formatXTick(tick);
+ return (
+ <text key={index} className="bubble-chart-tick" x={x} y={y} dy="1.5em">
+ {innerText}
+ </text>
+ );
+ });
+
+ return <g>{ticks}</g>;
+ }
+
+ renderYTicks(yTicks: Array<number>, xScale: Scale, yScale: Scale) {
+ if (!this.props.displayYTicks) {
+ return null;
+ }
+
+ const ticks = yTicks.map((tick, index) => {
+ const x = xScale.range()[0];
+ const y = yScale(tick);
+ const innerText = this.props.formatYTick(tick);
+ return (
+ <text
+ key={index}
+ className="bubble-chart-tick bubble-chart-tick-y"
+ x={x}
+ y={y}
+ dx="-0.5em"
+ dy="0.3em">
+ {innerText}
+ </text>
+ );
+ });
+
+ return <g>{ticks}</g>;
+ }
+
+ 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(this.props.xDomain || [0, d3.max(this.props.items, d => d.x)])
+ .range([0, availableWidth])
+ .nice();
+ const yScale = d3.scale
+ .linear()
+ .domain(this.props.yDomain || [0, d3.max(this.props.items, d => d.y)])
+ .range([availableHeight, 0])
+ .nice();
+ const sizeScale = d3.scale
+ .linear()
+ .domain(this.props.sizeDomain || [0, d3.max(this.props.items, d => d.size)])
+ .range(this.props.sizeRange);
+
+ const xScaleOriginal = xScale.copy();
+ const yScaleOriginal = yScale.copy();
+
+ xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
+ yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
+
+ const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
+ return (
+ <Bubble
+ 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}
+ />
+ );
+ });
+
+ const xTicks = this.getTicks(xScale, this.props.formatXTick);
+ const yTicks = this.getTicks(yScale, this.props.formatYTick);
+
+ return (
+ <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)}
+ {this.renderYGrid(yTicks, xScale, yScale)}
+ {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
+ {bubbles}
+ </g>
+ </svg>
+ );
+ }
+
+ render() {
+ return (
+ <AutoSizer disableHeight={true}>
+ {size => this.renderChart(size.width)}
+ </AutoSizer>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 { 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 = mount(<BubbleChart items={items} height={100} />);
+ expect(chart).toMatchSnapshot();
+});
--- /dev/null
+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>
+`;
+++ /dev/null
-/*
- * 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 { BubbleChart, Bubble } from '../bubble-chart';
-
-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);
-});
+++ /dev/null
-/*
- * 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 d3 from 'd3';
-import React from 'react';
-import sortBy from 'lodash/sortBy';
-import uniq from 'lodash/uniq';
-import { ResizeMixin } from './../mixins/resize-mixin';
-import { TooltipsMixin } from './../mixins/tooltips-mixin';
-
-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
- },
-
- 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})`}
- />
- );
- }
-});
-
-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) {
- 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) {
- 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) {
- 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) {
- if (!this.props.displayXGrid) {
- return null;
- }
-
- const lines = ticks.map((tick, index) => {
- const x = xScale(tick);
- const y1 = yScale.range()[0];
- const y2 = yScale.range()[1];
- return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid" />;
- });
-
- return <g ref="xGrid">{lines}</g>;
- },
-
- renderYGrid(ticks, xScale, yScale) {
- if (!this.props.displayYGrid) {
- return null;
- }
-
- const lines = ticks.map((tick, index) => {
- const y = yScale(tick);
- const x1 = xScale.range()[0];
- const x2 = xScale.range()[1];
- return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid" />;
- });
-
- return <g ref="yGrid">{lines}</g>;
- },
-
- renderXTicks(xTicks, xScale, yScale) {
- if (!this.props.displayXTicks) {
- return null;
- }
-
- const ticks = xTicks.map((tick, index) => {
- const x = xScale(tick);
- const y = yScale.range()[0];
- const innerText = this.props.formatXTick(tick);
- return (
- <text key={index} className="bubble-chart-tick" x={x} y={y} dy="1.5em">
- {innerText}
- </text>
- );
- });
-
- return <g>{ticks}</g>;
- },
-
- renderYTicks(yTicks, xScale, yScale) {
- if (!this.props.displayYTicks) {
- return null;
- }
-
- const ticks = yTicks.map((tick, index) => {
- const x = xScale.range()[0];
- const y = yScale(tick);
- const innerText = this.props.formatYTick(tick);
- return (
- <text
- key={index}
- className="bubble-chart-tick bubble-chart-tick-y"
- x={x}
- y={y}
- dx="-0.5em"
- 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];
-
- const xScale = d3.scale
- .linear()
- .domain([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)])
- .range([availableHeight, 0])
- .nice();
- const sizeScale = d3.scale
- .linear()
- .domain([0, d3.max(this.props.items, d => d.size)])
- .range(this.props.sizeRange);
-
- const xScaleOriginal = xScale.copy();
- const yScaleOriginal = yScale.copy();
-
- xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
- yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
-
- const bubbles = sortBy(this.props.items, b => -b.size).map((item, index) => {
- return (
- <Bubble
- key={index}
- link={item.link}
- tooltip={item.tooltip}
- x={xScale(item.x)}
- y={yScale(item.y)}
- r={sizeScale(item.size)}
- onClick={this.props.onBubbleClick}
- />
- );
- });
-
- const xTicks = this.getTicks(xScale, this.props.formatXTick);
- const yTicks = this.getTicks(yScale, this.props.formatYTick);
-
- return (
- <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderXGrid(xTicks, xScale, yScale)}
- {this.renderXTicks(xTicks, xScale, yScaleOriginal)}
- {this.renderYGrid(yTicks, xScale, yScale)}
- {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
- {bubbles}
- </g>
- </svg>
- );
- }
-});
'#b0d513',
'#00aa00'
];
+
+export const RATING_COLORS = ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#e00'];
.Select-placeholder,
:not(.Select--multi) > .Select-control .Select-value {
bottom: 0;
- color: #aaa;
left: 0;
line-height: @formControlHeight - 1px;
padding-left: 8px;
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)
#------------------------------------------------------------------------------