diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-03-23 17:21:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-23 17:21:20 +0100 |
commit | 6d2b71500ba2ac1c6ba22354a9551cb34d081a60 (patch) | |
tree | a6ac96ba6d318209462bcf5720a6e373514b357e /server/sonar-web/src/main/js/apps | |
parent | c0d7615caf06c79952049905af265adac61dadf4 (diff) | |
download | sonarqube-6d2b71500ba2ac1c6ba22354a9551cb34d081a60.tar.gz sonarqube-6d2b71500ba2ac1c6ba22354a9551cb34d081a60.zip |
MMF-721 Visualizations on the Projects page (#1826)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
27 files changed, 964 insertions, 46 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js index edf1b0def09..dee878337fc 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css index 5020b9b63ac..4263ca5d406 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/styles.css +++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index 77061055d28..96b4b5a4943 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js index 52398e9b340..f2fa86a3f2a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js @@ -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)); diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js index 8d242c75556..ffad9d0b8d5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js @@ -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)); diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js index 8b09599eb16..ee2cb6cf6d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js index bbae07c9429..ed924559a85 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js index 537bdafbc63..865ac9b200c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js @@ -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; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js b/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js new file mode 100644 index 00000000000..c672a377f9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js new file mode 100644 index 00000000000..ff8d2e9a408 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js new file mode 100644 index 00000000000..5d9c801df75 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap new file mode 100644 index 00000000000..211b0fbafb1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap new file mode 100644 index 00000000000..3e24cf0fcb0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap @@ -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" /> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/store/actions.js b/server/sonar-web/src/main/js/apps/projects/store/actions.js index dd1bb987851..ec113c07fab 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/actions.js +++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js @@ -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) => diff --git a/server/sonar-web/src/main/js/apps/projects/store/utils.js b/server/sonar-web/src/main/js/apps/projects/store/utils.js index 63ee07a6563..18a20aad6cb 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js @@ -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 => { diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index be23d753a3d..81b7af2c1cd 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -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; +}
\ No newline at end of file diff --git a/server/sonar-web/src/main/js/apps/projects/utils.js b/server/sonar-web/src/main/js/apps/projects/utils.js index cf393674817..bf3e0a14a26 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/utils.js @@ -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'); +}; diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js new file mode 100644 index 00000000000..6a704cb113f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js @@ -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" + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js b/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js new file mode 100644 index 00000000000..fb7682ff150 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js @@ -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" + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js b/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js new file mode 100644 index 00000000000..88e74540fa3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js @@ -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' }} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js b/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js new file mode 100644 index 00000000000..a9c9392940b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js new file mode 100644 index 00000000000..a44fbf96d7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js b/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js new file mode 100644 index 00000000000..ae36d5506ff --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js @@ -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' }} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js new file mode 100644 index 00000000000..5803e5bc13b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js new file mode 100644 index 00000000000..5ee27e1f90a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js new file mode 100644 index 00000000000..4d17b60df31 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js new file mode 100644 index 00000000000..4451e58f55f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js @@ -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" + /> + ); + } +} |