From 1ca69cefbef27f8f772650de4af6e6018ef85351 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 30 Aug 2017 18:26:07 +0200 Subject: [PATCH] avoid using redux in projects app --- .../sonar-web/src/main/js/api/components.ts | 27 +- server/sonar-web/src/main/js/api/measures.ts | 19 +- .../src/main/js/api/organizations.ts | 18 +- .../OrganizationFavoriteProjects.js | 60 ---- .../OrganizationFavoriteProjects.tsx} | 29 +- .../components/OrganizationProjects.js | 61 ---- .../components/OrganizationProjects.tsx} | 39 +-- .../apps/projects/components/AllProjects.tsx | 137 ++++++--- .../main/js/apps/projects/components/App.tsx | 37 ++- .../components/DefaultPageSelector.tsx | 28 +- .../projects/components/FavoriteFilter.tsx | 10 +- .../components/FavoriteFilterContainer.tsx | 28 -- ...ainer.ts => FavoriteProjectsContainer.tsx} | 7 +- .../apps/projects/components/PageHeader.tsx | 22 +- .../apps/projects/components/PageSidebar.tsx | 105 +++++-- ...PageHeaderContainer.ts => ProjectCard.tsx} | 23 +- .../components/ProjectCardContainer.tsx | 54 ---- .../components/ProjectCardLanguages.tsx | 62 ++-- .../projects/components/ProjectCardLeak.tsx | 36 +-- .../components/ProjectCardOrganization.tsx | 48 +++ .../components/ProjectCardOverall.tsx | 35 +-- .../components/ProjectCardOverallMeasures.tsx | 6 +- .../apps/projects/components/ProjectsList.tsx | 17 +- .../components/ProjectsListContainer.ts | 29 -- .../components/ProjectsListFooterContainer.ts | 40 --- .../components/__tests__/AllProjects-test.tsx | 105 +++---- .../__tests__/DefaultPageSelector-test.tsx | 12 +- .../__tests__/FavoriteFilter-test.tsx | 20 +- .../components/__tests__/PageHeader-test.tsx | 13 +- .../__tests__/ProjectCardLanguages-test.tsx | 10 +- .../__tests__/ProjectCardLeak-test.tsx | 26 +- .../__tests__/ProjectCardOverall-test.tsx | 28 +- .../__tests__/ProjectsList-test.tsx | 6 +- .../__snapshots__/AllProjects-test.tsx.snap | 68 ++++- .../__snapshots__/PageSidebar-test.tsx.snap | 13 +- .../ProjectCardLeak-test.tsx.snap | 8 + .../ProjectCardOverall-test.tsx.snap | 8 + .../ProjectCardOverallMeasures-test.tsx.snap | 4 +- .../__snapshots__/ProjectsList-test.tsx.snap | 20 +- .../apps/projects/filters/CoverageFilter.tsx | 12 +- .../projects/filters/DuplicationsFilter.tsx | 12 +- .../main/js/apps/projects/filters/Filter.tsx | 2 +- .../apps/projects/filters/FilterContainer.ts | 33 --- .../js/apps/projects/filters/IssuesFilter.tsx | 14 +- .../apps/projects/filters/LanguagesFilter.tsx | 19 +- .../filters/LanguagesFilterContainer.ts | 34 --- .../filters/MaintainabilityFilter.tsx | 4 + .../apps/projects/filters/NewLinesFilter.tsx | 12 +- .../filters/NewMaintainabilityFilter.tsx | 4 + .../projects/filters/NewReliabilityFilter.tsx | 4 + .../projects/filters/NewSecurityFilter.tsx | 4 + .../projects/filters/QualityGateFilter.tsx | 12 +- .../projects/filters/ReliabilityFilter.tsx | 4 + .../apps/projects/filters/SecurityFilter.tsx | 4 + .../js/apps/projects/filters/SizeFilter.tsx | 12 +- .../js/apps/projects/filters/TagsFilter.tsx | 3 +- .../projects/filters/TagsFilterContainer.ts | 32 -- .../filters/__tests__/IssuesFilter-test.tsx | 2 +- .../__tests__/LanguagesFilter-test.tsx | 37 +-- .../CoverageFilter-test.tsx.snap | 2 +- .../DuplicationsFilter-test.tsx.snap | 2 +- .../__snapshots__/IssuesFilter-test.tsx.snap | 3 +- .../NewLinesFilter-test.tsx.snap | 2 +- .../QualityGateFilter-test.tsx.snap | 2 +- .../__snapshots__/SizeFilter-test.tsx.snap | 2 +- .../src/main/js/apps/projects/query.ts | 256 ++++++++++++++++ .../main/js/apps/projects/store/actions.js | 240 --------------- .../main/js/apps/projects/store/facetsDuck.js | 88 ------ .../js/apps/projects/store/projectsDuck.js | 51 ---- .../main/js/apps/projects/store/reducer.js | 34 --- .../src/main/js/apps/projects/store/utils.js | 271 ----------------- .../src/main/js/apps/projects/types.ts | 15 +- .../src/main/js/apps/projects/utils.ts | 273 ++++++++++++++++++ .../visualizations/Visualizations.tsx | 2 +- .../visualizations/VisualizationsContainer.ts | 49 ---- .../visualizations/__tests__/Risk-test.tsx | 4 +- .../__tests__/SimpleBubbleChart-test.tsx | 4 +- .../controls/{Favorite.js => Favorite.tsx} | 35 +-- .../{FavoriteBase.js => FavoriteBase.tsx} | 38 +-- .../controls/__tests__/Favorite-test.tsx} | 12 +- ...riteBase-test.js => FavoriteBase-test.tsx} | 14 +- .../__snapshots__/Favorite-test.tsx.snap | 9 + ...est.js.snap => FavoriteBase-test.tsx.snap} | 2 - .../sonar-web/src/main/js/helpers/measures.ts | 2 +- .../src/main/js/store/components/actions.js | 32 -- .../src/main/js/store/components/reducer.js | 51 ---- .../src/main/js/store/measures/actions.js | 42 --- .../src/main/js/store/measures/reducer.js | 67 ----- .../src/main/js/store/rootReducer.js | 24 -- 89 files changed, 1424 insertions(+), 1782 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js rename server/sonar-web/src/main/js/apps/{projects/components/FavoriteProjectsContainer.ts => organizations/components/OrganizationFavoriteProjects.tsx} (60%) delete mode 100644 server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js rename server/sonar-web/src/main/js/apps/{projects/store/stateDuck.js => organizations/components/OrganizationProjects.tsx} (60%) delete mode 100644 server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx rename server/sonar-web/src/main/js/apps/projects/components/{AllProjectsContainer.ts => FavoriteProjectsContainer.tsx} (84%) rename server/sonar-web/src/main/js/apps/projects/components/{PageHeaderContainer.ts => ProjectCard.tsx} (65%) delete mode 100644 server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.tsx create mode 100644 server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx delete mode 100644 server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.ts delete mode 100644 server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.ts delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.ts delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.ts delete mode 100644 server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.ts create mode 100644 server/sonar-web/src/main/js/apps/projects/query.ts delete mode 100644 server/sonar-web/src/main/js/apps/projects/store/actions.js delete mode 100644 server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js delete mode 100644 server/sonar-web/src/main/js/apps/projects/store/projectsDuck.js delete mode 100644 server/sonar-web/src/main/js/apps/projects/store/reducer.js delete mode 100644 server/sonar-web/src/main/js/apps/projects/store/utils.js delete mode 100644 server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.ts rename server/sonar-web/src/main/js/components/controls/{Favorite.js => Favorite.tsx} (63%) rename server/sonar-web/src/main/js/components/controls/{FavoriteBase.js => FavoriteBase.tsx} (77%) rename server/sonar-web/src/main/js/{apps/projects/components/ProjectCardLanguagesContainer.ts => components/controls/__tests__/Favorite-test.tsx} (73%) rename server/sonar-web/src/main/js/components/controls/__tests__/{FavoriteBase-test.js => FavoriteBase-test.tsx} (96%) create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap rename server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/{FavoriteBase-test.js.snap => FavoriteBase-test.tsx.snap} (92%) delete mode 100644 server/sonar-web/src/main/js/store/components/actions.js delete mode 100644 server/sonar-web/src/main/js/store/components/reducer.js delete mode 100644 server/sonar-web/src/main/js/store/measures/actions.js delete mode 100644 server/sonar-web/src/main/js/store/measures/reducer.js diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 361713b815d..42e3b05ea6e 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -150,7 +150,32 @@ export function getMyProjects(data: RequestData): Promise { return getJSON(url, data); } -export function searchProjects(data: RequestData): Promise { +export interface Paging { + pageIndex: number; + pageSize: number; + total: number; +} + +export interface Component { + organization: string; + id: string; + key: string; + name: string; + isFavorite?: boolean; + analysisDate?: string; + tags: string[]; + visibility: string; + leakPeriodDate?: string; +} + +export interface Facet { + property: string; + values: Array<{ val: string; count: number }>; +} + +export function searchProjects( + data: RequestData +): Promise<{ components: Component[]; facets: Facet[]; paging: Paging }> { const url = '/api/components/search_projects'; return getJSON(url, data); } diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index fd4f5259d2f..df194d04a34 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -38,9 +38,24 @@ export function getMeasuresAndMeta( return getJSON('/api/measures/component', data); } -export function getMeasuresForProjects(projectKeys: string[], metricKeys: string[]): Promise { +export interface Period { + index: number; + value: string; +} + +export interface Measure { + component: string; + metric: string; + periods?: Period[]; + value?: string; +} + +export function getMeasuresForProjects( + projectKeys: string[], + metricKeys: string[] +): Promise { return getJSON('/api/measures/search', { projectKeys: projectKeys.join(), metricKeys: metricKeys.join() - }); + }).then(r => r.measures); } diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts index d16eb61e07e..408f6b65cb2 100644 --- a/server/sonar-web/src/main/js/api/organizations.ts +++ b/server/sonar-web/src/main/js/api/organizations.ts @@ -20,7 +20,23 @@ import { getJSON, post, postJSON, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -export function getOrganizations(organizations?: string[]): Promise { +interface GetOrganizationsResponse { + organizations: Array<{ + avatar?: string; + description?: string; + guarded: boolean; + key: string; + name: string; + url?: string; + }>; + paging: { + pageIndex: number; + pageSize: number; + total: number; + }; +} + +export function getOrganizations(organizations?: string[]): Promise { const data: RequestData = {}; if (organizations) { Object.assign(data, { organizations: organizations.join() }); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js deleted file mode 100644 index ef55c9023d0..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 FavoriteProjectsContainer from '../../projects/components/FavoriteProjectsContainer'; - -export default class OrganizationFavoriteProjects extends React.PureComponent { - /*:: props: { - children?: React.Element<*>, - currentUser: { isLoggedIn: boolean }, - location: Object, - organization: { - key: string - } - }; -*/ - - componentDidMount() { - const html = document.querySelector('html'); - if (html) { - html.classList.add('dashboard-page'); - } - } - - componentWillUnmount() { - const html = document.querySelector('html'); - if (html) { - html.classList.remove('dashboard-page'); - } - } - - render() { - return ( -
- -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.ts b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx similarity index 60% rename from server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.ts rename to server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx index fc3ad9992c6..d2413620662 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.ts +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx @@ -17,14 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { connect } from 'react-redux'; -import AllProjects from './AllProjects'; -import { getCurrentUser } from '../../../store/rootReducer'; -import { fetchProjects } from '../store/actions'; +import * as React from 'react'; +import App from '../../projects/components/App'; +import AllProjects from '../../projects/components/AllProjects'; -const mapStateToProps = (state: any) => ({ - isFavorite: true, - currentUser: getCurrentUser(state) -}); +interface Props { + location: { pathname: string; query: { [x: string]: string } }; + organization: { key: string }; +} -export default connect(mapStateToProps, { fetchProjects })(AllProjects); +export default function OrganizationFavoriteProjects(props: Props) { + return ( +
+ + + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js deleted file mode 100644 index a9ef3fca16f..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 AllProjectsContainer from '../../projects/components/AllProjectsContainer'; - -export default class OrganizationProjects extends React.PureComponent { - /*:: props: { - children?: React.Element<*>, - currentUser: { isLoggedIn: boolean }, - location: Object, - organization: { - key: string - } - }; -*/ - - componentDidMount() { - const html = document.querySelector('html'); - if (html) { - html.classList.add('dashboard-page'); - } - } - - componentWillUnmount() { - const html = document.querySelector('html'); - if (html) { - html.classList.remove('dashboard-page'); - } - } - - render() { - return ( -
- -
- ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/store/stateDuck.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx similarity index 60% rename from server/sonar-web/src/main/js/apps/projects/store/stateDuck.js rename to server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx index 2b1e5ad297c..07f220f0d96 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/stateDuck.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx @@ -17,24 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { createValue } from '../../../store/utils/generalReducers'; +import * as React from 'react'; +import App from '../../projects/components/App'; +import AllProjects from '../../projects/components/AllProjects'; -export const actions = { - UPDATE_STATE: 'projects/UPDATE_STATE' -}; +interface Props { + location: { pathname: string; query: { [x: string]: string } }; + organization: { key: string }; +} -export const updateState = changes => ({ - type: actions.UPDATE_STATE, - changes -}); - -export default createValue( - // should update - (state, action) => action.type === actions.UPDATE_STATE, - // should reset - () => false, - // get next value - (state, action) => ({ ...state, ...action.changes }), - // default value - {} -); +export default function OrganizationProjects(props: Props) { + return ( +
+ + + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index bca7874cba0..d0c700dbc1b 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -20,38 +20,46 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import Helmet from 'react-helmet'; -import PageHeaderContainer from './PageHeaderContainer'; -import ProjectsListContainer from './ProjectsListContainer'; -import ProjectsListFooterContainer from './ProjectsListFooterContainer'; +import PageHeader from './PageHeader'; +import ProjectsList from './ProjectsList'; import PageSidebar from './PageSidebar'; -import VisualizationsContainer from '../visualizations/VisualizationsContainer'; -import { parseUrlQuery } from '../store/utils'; +import Visualizations from '../visualizations/Visualizations'; +import ListFooter from '../../../components/controls/ListFooter'; import { translate } from '../../../helpers/l10n'; -import * as utils from '../utils'; import * as storage from '../../../helpers/storage'; import { RawQuery } from '../../../helpers/query'; import '../styles.css'; +import { Project, Facets } from '../types'; +import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils'; +import { parseUrlQuery, Query } from '../query'; interface Props { isFavorite: boolean; - location: { pathname: string; query: RawQuery }; - fetchProjects: (query: RawQuery, isFavorite: boolean, organization?: {}) => Promise; + location: { pathname: string; query: { [x: string]: string } }; organization?: { key: string }; - currentUser?: { isLoggedIn: boolean }; } interface State { - query: RawQuery; + facets?: Facets; + loading: boolean; + pageIndex?: number; + projects?: Project[]; + query: Query; + total?: number; } export default class AllProjects extends React.PureComponent { - state: State = { query: {} }; + mounted: boolean; + state: State = { loading: true, query: {} }; static contextTypes = { + currentUser: PropTypes.object.isRequired, + organizationsEnabled: PropTypes.bool, router: PropTypes.object.isRequired }; componentDidMount() { + this.mounted = true; this.handleQueryChange(true); const footer = document.getElementById('footer'); footer && footer.classList.add('page-footer-with-sidebar'); @@ -64,6 +72,7 @@ export default class AllProjects extends React.PureComponent { } componentWillUnmount() { + this.mounted = false; const footer = document.getElementById('footer'); footer && footer.classList.remove('page-footer-with-sidebar'); } @@ -74,7 +83,53 @@ export default class AllProjects extends React.PureComponent { getSort = () => this.state.query.sort || 'name'; - isFiltered = () => Object.keys(this.state.query).some(key => this.state.query[key] != null); + isFiltered = () => Object.values(this.state.query).some(value => value != undefined); + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + fetchProjects = (query: any) => { + this.setState({ loading: true, query }); + fetchProjects( + query, + this.props.isFavorite, + this.props.organization && this.props.organization.key + ).then(response => { + if (this.mounted) { + this.setState({ + facets: response.facets, + loading: false, + pageIndex: 1, + projects: response.projects, + total: response.total + }); + } + }, this.stopLoading); + }; + + fetchMoreProjects = () => { + const { pageIndex, projects, query } = this.state; + if (pageIndex && projects && query) { + this.setState({ loading: true }); + fetchProjects( + query, + this.props.isFavorite, + this.props.organization && this.props.organization.key, + pageIndex + 1 + ).then(response => { + if (this.mounted) { + this.setState({ + loading: false, + pageIndex: pageIndex + 1, + projects: [...projects, ...response.projects] + }); + } + }, this.stopLoading); + } + }; getSavedOptions = () => { const options: { @@ -106,9 +161,9 @@ export default class AllProjects extends React.PureComponent { if (this.state.query.view === 'leak' || view === 'leak') { if (this.state.query.sort) { - const sort = utils.parseSorting(this.state.query.sort); - if (utils.SORTING_SWITCH[sort.sortValue]) { - query.sort = (sort.sortDesc ? '-' : '') + utils.SORTING_SWITCH[sort.sortValue]; + const sort = parseSorting(this.state.query.sort); + if (SORTING_SWITCH[sort.sortValue]) { + query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue]; } } this.context.router.push({ pathname: this.props.location.pathname, query }); @@ -136,8 +191,7 @@ export default class AllProjects extends React.PureComponent { if (initialMount && !this.isFiltered() && savedOptionsSet) { this.context.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); } else { - this.setState({ query }); - this.props.fetchProjects(query, this.props.isFavorite, this.props.organization); + this.fetchProjects(query); } } @@ -159,6 +213,7 @@ export default class AllProjects extends React.PureComponent {
{
- @@ -194,23 +252,32 @@ export default class AllProjects extends React.PureComponent { renderMain = () => this.getView() === 'visualizations' ? (
- + {this.state.projects && ( + + )}
) : (
- - + )} +
); diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.tsx b/server/sonar-web/src/main/js/apps/projects/components/App.tsx index c3ebbadffc8..0e605b910e8 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/App.tsx @@ -18,8 +18,35 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { connect } from 'react-redux'; +import * as PropTypes from 'prop-types'; +import { + getCurrentUser, + getLanguages, + areThereCustomOrganizations +} from '../../../store/rootReducer'; + +interface Props { + currentUser: { isLoggedIn: boolean }; + languages: { [key: string]: { key: string; name: string } }; + organizationsEnabled: boolean; +} + +class App extends React.PureComponent { + static childContextTypes = { + currentUser: PropTypes.object.isRequired, + languages: PropTypes.object.isRequired, + organizationsEnabled: PropTypes.bool + }; + + getChildContext() { + return { + currentUser: this.props.currentUser, + languages: this.props.languages, + organizationsEnabled: this.props.organizationsEnabled + }; + } -export default class App extends React.PureComponent { componentDidMount() { const elem = document.querySelector('html'); if (elem) { @@ -38,3 +65,11 @@ export default class App extends React.PureComponent { return
{this.props.children}
; } } + +const mapStateToProps = (state: any) => ({ + currentUser: getCurrentUser(state), + languages: getLanguages(state), + organizationsEnabled: areThereCustomOrganizations(state) +}); + +export default connect(mapStateToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx index f8e73ba7fad..c5d4197f1be 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx @@ -18,16 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import * as PropTypes from 'prop-types'; -import AllProjectsContainer from './AllProjectsContainer'; -import { getCurrentUser } from '../../../store/rootReducer'; +import AllProjects from './AllProjects'; import { isFavoriteSet, isAllSet } from '../../../helpers/storage'; import { searchProjects } from '../../../api/components'; interface Props { - currentUser: { isLoggedIn: boolean }; - location: { query: { [x: string]: string } }; + location: { pathname: string; query: { [x: string]: string } }; } interface State { @@ -35,8 +32,9 @@ interface State { shouldForceSorting?: string; } -class DefaultPageSelector extends React.PureComponent { +export default class DefaultPageSelector extends React.PureComponent { static contextTypes = { + currentUser: PropTypes.object.isRequired, router: PropTypes.object.isRequired }; @@ -69,7 +67,7 @@ class DefaultPageSelector extends React.PureComponent { if (Object.keys(this.props.location.query).length > 0) { // show ALL projects when there are some filters this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined }); - } else if (!this.props.currentUser.isLoggedIn) { + } else if (!this.context.currentUser.isLoggedIn) { // show ALL projects if user is anonymous if (!this.props.location.query || !this.props.location.query.sort) { // force default sorting to last analysis date @@ -98,21 +96,7 @@ class DefaultPageSelector extends React.PureComponent { if (shouldBeRedirected == null || shouldBeRedirected === true || shouldForceSorting != null) { return null; } else { - return ( - - ); + return ; } } } - -const mapStateToProps = (state: any) => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(DefaultPageSelector); - -export const UnconnectedDefaultPageSelector = DefaultPageSelector; diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx index 372d0613c3e..25c9d92209b 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx @@ -18,18 +18,22 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as PropTypes from 'prop-types'; import { IndexLink, Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; import { saveAll, saveFavorite } from '../../../helpers/storage'; import { RawQuery } from '../../../helpers/query'; interface Props { - user: { isLoggedIn?: boolean }; organization?: { key: string }; - query: RawQuery; + query?: RawQuery; } export default class FavoriteFilter extends React.PureComponent { + static contextTypes = { + currentUser: PropTypes.object.isRequired + }; + handleSaveFavorite = () => { if (!this.props.organization) { saveFavorite(); @@ -43,7 +47,7 @@ export default class FavoriteFilter extends React.PureComponent { }; render() { - if (!this.props.user.isLoggedIn) { + if (!this.context.currentUser.isLoggedIn) { return null; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx deleted file mode 100644 index 515fbf0ae8c..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 FavoriteFilter from './FavoriteFilter'; -import { getCurrentUser } from '../../../store/rootReducer'; - -const mapStateToProps = (state: any) => ({ - user: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(FavoriteFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.ts b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx similarity index 84% rename from server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.ts rename to server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx index 83d5f49ba66..1bfa07b70f2 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.ts +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx @@ -17,8 +17,9 @@ * 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 * as React from 'react'; import AllProjects from './AllProjects'; -import { fetchProjects } from '../store/actions'; -export default connect(null, { fetchProjects })(AllProjects); +export default function FavoriteProjectsContainer(props: any) { + return ; +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx index f9fd94bd3dd..264cb863e52 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx @@ -25,25 +25,26 @@ import PerspectiveSelect from './PerspectiveSelect'; import ProjectsSortingSelect from './ProjectsSortingSelect'; import { translate } from '../../../helpers/l10n'; import { RawQuery } from '../../../helpers/query'; +import { Project } from '../types'; interface Props { currentUser?: { isLoggedIn: boolean }; isFavorite?: boolean; + loading: boolean; onPerspectiveChange: (x: { view: string; visualization?: string }) => void; + onSortChange: (sort: string, desc: boolean) => void; organization?: { key: string }; - projects: Array; - projectsAppState: { loading: boolean; total?: number }; + projects?: Project[]; query: RawQuery; - onSortChange: (sort: string, desc: boolean) => void; selectedSort: string; + total?: number; view: string; visualization?: string; } export default function PageHeader(props: Props) { - const { projectsAppState, projects, currentUser, view } = props; - const limitReached = - projects != null && projectsAppState.total != null && projects.length < projectsAppState.total; + const { loading, total, projects, currentUser, view } = props; + const limitReached = projects != null && total != null && projects.length < total; const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date'; return ( @@ -86,14 +87,13 @@ export default function PageHeader(props: Props) {
- {!!props.projectsAppState.loading && } + {loading && } - {props.projectsAppState.total != null && ( + {total != null && ( - {props.projectsAppState.total}{' '} - {translate('projects._projects')} + {total} {translate('projects._projects')} )}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx index e1d6e6dddb0..6da09bf1157 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx @@ -19,8 +19,9 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import FavoriteFilterContainer from './FavoriteFilterContainer'; -import LanguagesFilterContainer from '../filters/LanguagesFilterContainer'; +import { flatMap } from 'lodash'; +import FavoriteFilter from './FavoriteFilter'; +import LanguagesFilter from '../filters/LanguagesFilter'; import CoverageFilter from '../filters/CoverageFilter'; import DuplicationsFilter from '../filters/DuplicationsFilter'; import MaintainabilityFilter from '../filters/MaintainabilityFilter'; @@ -34,11 +35,13 @@ import QualityGateFilter from '../filters/QualityGateFilter'; import ReliabilityFilter from '../filters/ReliabilityFilter'; import SecurityFilter from '../filters/SecurityFilter'; import SizeFilter from '../filters/SizeFilter'; -import TagsFilterContainer from '../filters/TagsFilterContainer'; +import TagsFilter from '../filters/TagsFilter'; import { translate } from '../../../helpers/l10n'; import { RawQuery } from '../../../helpers/query'; +import { Facets } from '../types'; interface Props { + facets?: Facets; isFavorite: boolean; organization?: { key: string }; query: RawQuery; @@ -47,14 +50,15 @@ interface Props { } export default function PageSidebar(props: Props) { - const { query, isFavorite, organization, view, visualization } = props; + const { facets, query, isFavorite, organization, view, visualization } = props; const isFiltered = Object.keys(query) .filter(key => !['view', 'visualization', 'sort'].includes(key)) .some(key => query[key] != null); const isLeakView = view === 'leak'; const basePathName = organization ? `/organizations/${organization.key}/projects` : '/projects'; const pathname = basePathName + (isFavorite ? '/favorite' : ''); - const facetProps = { query, isFavorite, organization }; + const maxFacetValue = getMaxFacetValue(facets); + const facetProps = { isFavorite, maxFacetValue, organization, query }; let linkQuery: RawQuery | undefined = undefined; if (view !== 'overall') { @@ -67,7 +71,7 @@ export default function PageSidebar(props: Props) { return (
- +
{isFiltered && ( @@ -80,25 +84,84 @@ export default function PageSidebar(props: Props) {

{translate('filters')}

- + {!isLeakView && [ - , - , - , - , - , - + , + , + , + , + , + ]} {isLeakView && [ - , - , - , - , - , - + , + , + , + , + , + ]} - - + +
); } + +function getMaxFacetValue(facets?: Facets) { + return facets && Math.max(...flatMap(Object.values(facets), facet => Object.values(facet))); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.ts b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx similarity index 65% rename from server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.ts rename to server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx index 428e43364d3..8ef9f193b32 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.ts +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx @@ -17,13 +17,20 @@ * 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 PageHeader from './PageHeader'; -import { getProjects, getProjectsAppState } from '../../../store/rootReducer'; +import * as React from 'react'; +import ProjectCardLeak from './ProjectCardLeak'; +import ProjectCardOverall from './ProjectCardOverall'; +import { Project } from '../types'; -const mapStateToProps = (state: any) => ({ - projects: getProjects(state), - projectsAppState: getProjectsAppState(state) -}); +interface Props { + organization?: { key: string }; + project: Project; + type?: string; +} -export default connect(mapStateToProps)(PageHeader); +export default function ProjectCard(props: Props) { + if (props.type === 'leak') { + return ; + } + return ; +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.tsx deleted file mode 100644 index 429d7abe1ae..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 * as React from 'react'; -import { connect } from 'react-redux'; -import ProjectCardLeak from './ProjectCardLeak'; -import ProjectCardOverall from './ProjectCardOverall'; -import { getComponent, getComponentMeasures } from '../../../store/rootReducer'; - -interface Props { - measures?: { [key: string]: string }; - organization?: { key: string }; - project?: { - analysisDate?: string; - key: string; - leakPeriodDate?: string; - name: string; - tags: Array; - isFavorite?: boolean; - organization?: string; - visibility?: string; - }; - type?: string; -} - -function ProjectCard(props: Props) { - if (props.type === 'leak') { - return ; - } - return ; -} - -const mapStateToProps = (state: any, ownProps: any) => ({ - project: getComponent(state, ownProps.projectKey), - measures: getComponentMeasures(state, ownProps.projectKey) -}); - -export default connect(mapStateToProps)(ProjectCard); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx index e676d7378f0..4bf823864e0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as PropTypes from 'prop-types'; import { sortBy } from 'lodash'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; @@ -28,40 +29,45 @@ interface Languages { interface Props { distribution?: string; - languages: Languages; } -export default function ProjectCardLanguages({ distribution, languages }: Props) { - if (distribution == undefined) { - return null; - } +export default class ProjectCardLanguages extends React.PureComponent { + static contextTypes = { + languages: PropTypes.object.isRequired + }; + + render() { + if (this.props.distribution == undefined) { + return null; + } - const parsedLanguages = distribution.split(';').map(item => item.split('=')); - const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => - getLanguageName(languages, l[0]) - ); + const parsedLanguages = this.props.distribution.split(';').map(item => item.split('=')); + const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => + getLanguageName(this.context.languages, l[0]) + ); - const tooltip = ( - - {finalLanguages.map(language => ( - - {language} -
-
- ))} -
- ); + const tooltip = ( + + {finalLanguages.map(language => ( + + {language} +
+
+ ))} +
+ ); - const languagesText = - finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); + const languagesText = + finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); - return ( -
- - {languagesText} - -
- ); + return ( +
+ + {languagesText} + +
+ ); + } } function getLanguageName(languages: Languages, key: string): string { diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index aefda0b2ac2..f0e8fca7d54 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -24,37 +24,25 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; -import FavoriteContainer from '../../../components/controls/FavoriteContainer'; -import Organization from '../../../components/shared/Organization'; +import ProjectCardOrganization from './ProjectCardOrganization'; +import Favorite from '../../../components/controls/Favorite'; import TagsList from '../../../components/tags/TagsList'; import PrivateBadge from '../../../components/common/PrivateBadge'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { Project } from '../types'; interface Props { - measures?: { [key: string]: string }; organization?: { key: string }; - project?: { - analysisDate?: string; - key: string; - leakPeriodDate?: string; - name: string; - tags: Array; - isFavorite?: boolean; - organization?: string; - visibility?: string; - }; + project: Project; } -export default function ProjectCardLeak({ measures, organization, project }: Props) { - if (project == undefined) { - return null; - } +export default function ProjectCardLeak({ organization, project }: Props) { + const { measures } = project; const isProjectAnalyzed = project.analysisDate != null; const isPrivate = project.visibility === 'private'; const hasLeakPeriodStart = project.leakPeriodDate != undefined; const hasTags = project.tags.length > 0; - const showOrganization = organization == undefined && project.organization != undefined; // check for particular measures because only some measures can be loaded // if coming from visualizations tab @@ -69,14 +57,14 @@ export default function ProjectCardLeak({ measures, organization, project }: Pro
{project.isFavorite != null && ( - + )}

- {showOrganization && ( - - - - )} + {!organization && } {project.name}

{displayQualityGate && } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx new file mode 100644 index 00000000000..ce8c98ffd81 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import OrganizationLink from '../../../components/ui/OrganizationLink'; + +interface Props { + organization?: { key: string; name: string }; +} + +export default class ProjectCardOrganization extends React.PureComponent { + static contextTypes = { + organizationsEnabled: PropTypes.bool + }; + + render() { + const { organization } = this.props; + const { organizationsEnabled } = this.context; + + if (!organization || !organizationsEnabled) { + return null; + } + + return ( + + {organization.name} + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx index c52c09a3ff6..75f0f8375d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx @@ -23,35 +23,24 @@ import { Link } from 'react-router'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardOverallMeasures from './ProjectCardOverallMeasures'; -import FavoriteContainer from '../../../components/controls/FavoriteContainer'; -import Organization from '../../../components/shared/Organization'; +import ProjectCardOrganization from './ProjectCardOrganization'; +import Favorite from '../../../components/controls/Favorite'; import TagsList from '../../../components/tags/TagsList'; import PrivateBadge from '../../../components/common/PrivateBadge'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { Project } from '../types'; interface Props { - measures?: { [key: string]: string }; organization?: { key: string }; - project?: { - analysisDate?: string; - key: string; - name: string; - tags: Array; - isFavorite?: boolean; - organization?: string; - visibility?: string; - }; + project: Project; } -export default function ProjectCardOverall({ measures, organization, project }: Props) { - if (project == undefined) { - return null; - } +export default function ProjectCardOverall({ organization, project }: Props) { + const { measures } = project; const isProjectAnalyzed = project.analysisDate != undefined; const isPrivate = project.visibility === 'private'; const hasTags = project.tags.length > 0; - const showOrganization = organization == undefined && project.organization != undefined; // check for particular measures because only some measures can be loaded // if coming from visualizations tab @@ -69,14 +58,14 @@ export default function ProjectCardOverall({ measures, organization, project }:
{project.isFavorite != undefined && ( - + )}

- {showOrganization && ( - - - - )} + {!organization && } {project.name}

{displayQualityGate && } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx index 4c5a242a638..15c2565cc31 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer'; +import ProjectCardLanguages from './ProjectCardLanguages'; import Measure from '../../../components/measure/Measure'; import Rating from '../../../components/ui/Rating'; import CoverageRating from '../../../components/ui/CoverageRating'; @@ -125,9 +125,7 @@ export default function ProjectCardOverallMeasures({ measures }: Props) { />
- +
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx index 08db1a07322..6636307001a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx @@ -18,17 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ProjectCardContainer from './ProjectCardContainer'; +import ProjectCard from './ProjectCard'; import NoFavoriteProjects from './NoFavoriteProjects'; import EmptyInstance from './EmptyInstance'; import EmptySearch from '../../../components/common/EmptySearch'; +import { Project } from '../types'; interface Props { cardType?: string; isFavorite: boolean; isFiltered: boolean; organization?: { key: string }; - projects?: string[]; + projects: Project[]; } export default class ProjectsList extends React.PureComponent { @@ -45,17 +46,13 @@ export default class ProjectsList extends React.PureComponent { render() { const { projects } = this.props; - if (projects == undefined) { - return null; - } - return (
{projects.length > 0 ? ( - projects.map(projectKey => ( - ( + diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.ts b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.ts deleted file mode 100644 index 76c02ecfc10..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 ProjectsList from './ProjectsList'; -import { getProjects, getProjectsAppState } from '../../../store/rootReducer'; - -const mapStateToProps = (state: any) => ({ - projects: getProjects(state), - total: getProjectsAppState(state).total -}); - -export default connect(mapStateToProps)(ProjectsList); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.ts b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.ts deleted file mode 100644 index fb296456692..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { getProjects, getProjectsAppState } from '../../../store/rootReducer'; -import { fetchMoreProjects } from '../store/actions'; -import ListFooter from '../../../components/controls/ListFooter'; - -const mapStateToProps = (state: any) => { - const projects = getProjects(state); - const appState = getProjectsAppState(state); - return { - count: projects != null ? projects.length : 0, - total: appState.total != null ? appState.total : 0, - ready: !appState.loading - }; -}; - -const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ - loadMore: () => - dispatch(fetchMoreProjects(ownProps.query, ownProps.isFavorite, ownProps.organization)) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ListFooter); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index fd16bdac37d..5afb5f49874 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -17,19 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -jest.mock('../ProjectsListContainer', () => ({ - default: function ProjectsListContainer() { +jest.mock('../ProjectsList', () => ({ + default: function ProjectsList() { return null; } })); -jest.mock('../ProjectsListFooterContainer', () => ({ - default: function ProjectsListFooterContainer() { - return null; - } -})); -jest.mock('../PageHeaderContainer', () => ({ - default: function PageHeaderContainer() { +jest.mock('../PageHeader', () => ({ + default: function PageHeader() { return null; } })); @@ -40,6 +35,12 @@ jest.mock('../PageSidebar', () => ({ } })); +jest.mock('../../utils', () => { + const utils = require.requireActual('../../utils'); + utils.fetchProjects = jest.fn(() => Promise.resolve({ projects: [] })); + return utils; +}); + jest.mock('../../../../helpers/storage', () => ({ getSort: () => null, getView: jest.fn(() => null), @@ -54,51 +55,46 @@ import { mount, shallow } from 'enzyme'; import AllProjects from '../AllProjects'; import { getView, saveSort, saveView, saveVisualization } from '../../../../helpers/storage'; +const fetchProjects = require('../../utils').fetchProjects as jest.Mock; + beforeEach(() => { (getView as jest.Mock).mockImplementation(() => null); (saveSort as jest.Mock).mockClear(); (saveView as jest.Mock).mockClear(); (saveVisualization as jest.Mock).mockClear(); + fetchProjects.mockClear(); }); it('renders', () => { - const wrapper = shallow( - , - { context: { router: {} } } - ); + const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); wrapper.setState({ query: { view: 'visualizations' } }); expect(wrapper).toMatchSnapshot(); }); it('fetches projects', () => { - const fetchProjects = jest.fn(); - mountRender({ fetchProjects }); + mountRender(); expect(fetchProjects).lastCalledWith( { - coverage: null, - duplications: null, - gate: null, - languages: null, - maintainability: null, - new_coverage: null, - new_duplications: null, - new_lines: null, - new_maintainability: null, - new_reliability: null, - new_security: null, - reliability: null, - search: null, - security: null, - size: null, - sort: null, - tags: null, + coverage: undefined, + duplications: undefined, + gate: undefined, + languages: undefined, + maintainability: undefined, + new_coverage: undefined, + new_duplications: undefined, + new_lines: undefined, + new_maintainability: undefined, + new_reliability: undefined, + new_security: undefined, + reliability: undefined, + search: undefined, + security: undefined, + size: undefined, + sort: undefined, + tags: undefined, view: undefined, - visualization: null + visualization: undefined }, false, undefined @@ -114,16 +110,16 @@ it('redirects to the saved search', () => { it('changes sort', () => { const push = jest.fn(); - const wrapper = mountRender({}, push); - wrapper.find('PageHeaderContainer').prop('onSortChange')('size', false); + const wrapper = shallowRender({}, push); + wrapper.find('PageHeader').prop('onSortChange')('size', false); expect(push).lastCalledWith({ pathname: '/projects', query: { sort: 'size' } }); expect(saveSort).lastCalledWith('size'); }); it('changes perspective to leak', () => { const push = jest.fn(); - const wrapper = mountRender({}, push); - wrapper.find('PageHeaderContainer').prop('onPerspectiveChange')({ view: 'leak' }); + const wrapper = shallowRender({}, push); + wrapper.find('PageHeader').prop('onPerspectiveChange')({ view: 'leak' }); expect(push).lastCalledWith({ pathname: '/projects', query: { view: 'leak', visualization: undefined } @@ -135,11 +131,9 @@ it('changes perspective to leak', () => { it('updates sorting when changing perspective from leak', () => { const push = jest.fn(); - const wrapper = mountRender( - { location: { pathname: '/projects', query: { sort: 'new_coverage', view: 'leak' } } }, - push - ); - wrapper.find('PageHeaderContainer').prop('onPerspectiveChange')({ + const wrapper = shallowRender({}, push); + wrapper.setState({ query: { sort: 'new_coverage', view: 'leak' } }); + wrapper.find('PageHeader').prop('onPerspectiveChange')({ view: undefined }); expect(push).lastCalledWith({ @@ -153,8 +147,8 @@ it('updates sorting when changing perspective from leak', () => { it('changes perspective to risk visualization', () => { const push = jest.fn(); - const wrapper = mountRender({}, push); - wrapper.find('PageHeaderContainer').prop('onPerspectiveChange')({ + const wrapper = shallowRender({}, push); + wrapper.find('PageHeader').prop('onPerspectiveChange')({ view: 'visualizations', visualization: 'risk' }); @@ -175,6 +169,19 @@ function mountRender(props: any = {}, push: Function = jest.fn(), replace: Funct location={{ pathname: '/projects', query: {} }} {...props} />, - { context: { router: { push, replace } } } + { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } } ); } + +function shallowRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) { + const wrapper = shallow( + , + { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } } + ); + wrapper.setState({ + loading: false, + projects: [{ key: 'foo', measures: {}, name: 'Foo' }], + total: 0 + }); + return wrapper; +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx index a7af9fce1e6..ce03b94e0b3 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx @@ -17,8 +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. */ -jest.mock('../AllProjectsContainer', () => ({ - default: function AllProjectsContainer() { +jest.mock('../AllProjects', () => ({ + default: function AllProjects() { return null; } })); @@ -34,7 +34,7 @@ jest.mock('../../../../api/components', () => ({ import * as React from 'react'; import { mount } from 'enzyme'; -import { UnconnectedDefaultPageSelector } from '../DefaultPageSelector'; +import DefaultPageSelector from '../DefaultPageSelector'; import { doAsync } from '../../../../helpers/testUtils'; const isFavoriteSet = require('../../../../helpers/storage').isFavoriteSet as jest.Mock; @@ -55,7 +55,7 @@ it('shows all projects with existing filter', () => { it('shows all projects sorted by analysis date for anonymous', () => { const replace = jest.fn(); mountRender({ isLoggedIn: false }, undefined, replace); - expect(replace).lastCalledWith({ query: { sort: '-analysis_date' } }); + expect(replace).lastCalledWith({ pathname: '/projects', query: { sort: '-analysis_date' } }); }); it('shows favorite projects', () => { @@ -83,7 +83,7 @@ it('fetches favorites', () => { }); function mountRender(user: any = { isLoggedIn: true }, query: any = {}, replace: any = jest.fn()) { - return mount(, { - context: { router: { replace } } + return mount(, { + context: { currentUser: user, router: { replace } } }); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx index 49e81f03b90..b7971fb416a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx @@ -28,7 +28,7 @@ import FavoriteFilter from '../FavoriteFilter'; import { saveAll, saveFavorite } from '../../../../helpers/storage'; import { click } from '../../../../helpers/testUtils'; -const user = { isLoggedIn: true }; +const currentUser = { isLoggedIn: true }; const query = { size: 1 }; beforeEach(() => { @@ -37,11 +37,11 @@ beforeEach(() => { }); it('renders for logged in user', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow(, { context: { currentUser } })).toMatchSnapshot(); }); it('saves last selection', () => { - const wrapper = shallow(); + const wrapper = shallow(, { context: { currentUser } }); click(wrapper.find('#favorite-projects')); expect(saveFavorite).toBeCalled(); click(wrapper.find('#all-projects')); @@ -50,14 +50,16 @@ it('saves last selection', () => { it('handles organization', () => { expect( - shallow() + shallow(, { + context: { currentUser } + }) ).toMatchSnapshot(); }); it('does not save last selection with organization', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(, { + context: { currentUser } + }); click(wrapper.find('#favorite-projects')); expect(saveFavorite).not.toBeCalled(); click(wrapper.find('#all-projects')); @@ -65,5 +67,7 @@ it('does not save last selection with organization', () => { }); it('does not render for anonymous', () => { - expect(shallow()).toMatchSnapshot(); + expect( + shallow(, { context: { currentUser: { isLoggedIn: false } } }) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx index 48e9a86b3b2..86ef27cc3a5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx @@ -26,12 +26,12 @@ it('should render correctly', () => { }); it('should render correctly while loading', () => { - expect(shallowRender({ projectsAppState: { loading: true, total: 2 } })).toMatchSnapshot(); + expect(shallowRender({ loading: true, total: 2 })).toMatchSnapshot(); }); it('should not render projects total', () => { expect( - shallowRender({ projectsAppState: {} }) + shallowRender({ total: undefined }) .find('#projects-total') .exists() ).toBeFalsy(); @@ -41,7 +41,7 @@ it('should render disabled sorting options for visualizations', () => { expect( shallowRender({ open: true, - projectsAppState: {}, + total: undefined, view: 'visualizations', visualization: 'coverage' }) @@ -53,7 +53,6 @@ it('should render switch the default sorting option for anonymous users', () => shallowRender({ currentUser: { isLoggedIn: true }, open: true, - projectsAppState: {}, visualization: 'risk' }).find('ProjectsSortingSelect') ).toMatchSnapshot(); @@ -62,22 +61,22 @@ it('should render switch the default sorting option for anonymous users', () => shallowRender({ currentUser: { isLoggedIn: false }, open: true, - projectsAppState: {}, view: 'leak', visualization: 'risk' }).find('ProjectsSortingSelect') ).toMatchSnapshot(); }); -function shallowRender(props?: any) { +function shallowRender(props?: {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx index 76b724f3bce..cca1d63feb1 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx @@ -28,26 +28,26 @@ const languages = { it('renders', () => { expect( - shallow() + shallow(, { context: { languages } }) ).toMatchSnapshot(); }); it('sorts languages', () => { expect( - shallow() + shallow(, { context: { languages } }) ).toMatchSnapshot(); }); it('handles unknown languages', () => { expect( - shallow() + shallow(, { context: { languages } }) ).toMatchSnapshot(); expect( - shallow() + shallow(, { context: { languages } }) ).toMatchSnapshot(); }); it('does not render', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow(, { context: { languages } })).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx index 41b383216dc..8a9a75cf1cd 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx @@ -21,13 +21,6 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ProjectCardLeak from '../ProjectCardLeak'; -const PROJECT = { - analysisDate: '2017-01-01', - leakPeriodDate: '2016-12-01', - key: 'foo', - name: 'Foo', - tags: [] -}; const MEASURES = { alert_status: 'OK', reliability_rating: '1.0', @@ -35,8 +28,19 @@ const MEASURES = { new_bugs: '12' }; +const PROJECT = { + analysisDate: '2017-01-01', + leakPeriodDate: '2016-12-01', + key: 'foo', + measures: MEASURES, + name: 'Foo', + organization: { key: 'org', name: 'org' }, + tags: [], + visibility: 'public' +}; + it('should display analysis date and leak start date', () => { - const card = shallow(); + const card = shallow(); expect(card.find('.project-card-dates').exists()).toBeTruthy(); expect(card.find('.project-card-dates').find('DateFromNow')).toHaveLength(1); expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1); @@ -44,14 +48,14 @@ it('should display analysis date and leak start date', () => { it('should not display analysis date or leak start date', () => { const project = { ...PROJECT, analysisDate: undefined }; - const card = shallow(); + const card = shallow(); expect(card.find('.project-card-dates').exists()).toBeFalsy(); }); it('should display loading', () => { const measures = { alert_status: 'OK', reliability_rating: '1.0', sqale_rating: '1.0' }; expect( - shallow() + shallow() .find('.boxed-group') .hasClass('boxed-group-loading') ).toBeTruthy(); @@ -76,5 +80,5 @@ it('should private badge', () => { }); it('should display the leak measures and quality gate', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx index a1dcfb897b8..7a3e3bb3baf 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx @@ -21,12 +21,6 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ProjectCardOverall from '../ProjectCardOverall'; -const PROJECT = { - analysisDate: '2017-01-01', - key: 'foo', - name: 'Foo', - tags: [] -}; const MEASURES = { alert_status: 'OK', reliability_rating: '1.0', @@ -34,14 +28,24 @@ const MEASURES = { new_bugs: '12' }; +const PROJECT = { + analysisDate: '2017-01-01', + key: 'foo', + measures: MEASURES, + name: 'Foo', + organization: { key: 'org', name: 'org' }, + tags: [], + visibility: 'public' +}; + it('should display analysis date (and not leak period) when defined', () => { expect( - shallow() + shallow() .find('.project-card-dates') .exists() ).toBeTruthy(); expect( - shallow() + shallow() .find('.project-card-dates') .exists() ).toBeFalsy(); @@ -49,12 +53,12 @@ it('should display analysis date (and not leak period) when defined', () => { it('should display loading', () => { expect( - shallow() + shallow() .find('.boxed-group') .hasClass('boxed-group-loading') ).toBeTruthy(); expect( - shallow() + shallow() .find('.boxed-group') .hasClass('boxed-group-loading') ).toBeTruthy(); @@ -63,7 +67,7 @@ it('should display loading', () => { it('should not display the quality gate', () => { const project = { ...PROJECT, analysisDate: undefined }; expect( - shallow() + shallow() .find('ProjectCardOverallQualityGate') .exists() ).toBeFalsy(); @@ -88,5 +92,5 @@ it('should private badge', () => { }); it('should display the overall measures and quality gate', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsList-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsList-test.tsx index d15a866c1cb..4ad9127939c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsList-test.tsx @@ -25,10 +25,6 @@ it('renders', () => { expect(shallowRender()).toMatchSnapshot(); }); -it('does not render without projects', () => { - expect(shallow()).toMatchSnapshot(); -}); - it('renders different types of "no projects"', () => { expect(shallowRender({ projects: [] })).toMatchSnapshot(); expect(shallowRender({ projects: [], isFiltered: true })).toMatchSnapshot(); @@ -41,7 +37,7 @@ function shallowRender(props?: any) { cardType="overall" isFavorite={false} isFiltered={false} - projects={['foo', 'bar']} + projects={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index 70ce07e9dab..2561502915e 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -47,12 +47,28 @@ exports[`renders 1`] = `
- @@ -62,14 +78,25 @@ exports[`renders 1`] = `
- -
@@ -127,16 +154,32 @@ exports[`renders 2`] = `
- @@ -146,7 +189,18 @@ exports[`renders 2`] = `
-
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap index aa067968a43..5cd3d236fb0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap @@ -27,7 +27,7 @@ exports[`reset function should work correctly with view and visualizations 2`] = exports[`should render \`leak\` view correctly 1`] = `
- - - - +
@@ -198,8 +198,9 @@ exports[`should render correctly 1`] = ` "size": "3", } } + value="3" /> - - + + - +
@@ -281,7 +281,7 @@ exports[`should render ncloc correctly 1`] = `
- +
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap index 64edfbdf4da..b5a2c7729c5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap @@ -1,17 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`does not render without projects 1`] = `null`; - exports[`renders 1`] = `
- -
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx index b8784492319..9f6b3bca427 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.tsx @@ -18,26 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import FilterContainer from './FilterContainer'; +import Filter from './Filter'; import FilterHeader from './FilterHeader'; -import { Facet } from './Filter'; import CoverageRating from '../../../components/ui/CoverageRating'; import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; import { translate } from '../../../helpers/l10n'; +import { Facet } from '../types'; export interface Props { className?: string; + facet?: Facet; isFavorite?: boolean; + maxFacetValue?: number; organization?: { key: string }; property?: string; query: { [x: string]: any }; + value?: any; } export default function CoverageFilter(props: Props) { const { property = 'coverage' } = props; return ( - ({ - value: ownProps.query[ownProps.property], - facet: getProjectsAppFacetByProperty(state, ownProps.property), - maxFacetValue: getProjectsAppMaxFacetValue(state) -}); - -export default connect(mapStateToProps)(Filter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx index 2a139d6cb00..5e14ac24774 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.tsx @@ -18,25 +18,31 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import FilterContainer from './FilterContainer'; +import Filter from './Filter'; import FilterHeader from './FilterHeader'; import Rating from '../../../components/ui/Rating'; import { translate } from '../../../helpers/l10n'; -import { Facet } from './Filter'; +import { Facet } from '../types'; interface Props { className?: string; + facet?: Facet; headerDetail?: React.ReactNode; isFavorite?: boolean; + maxFacetValue?: number; name: string; organization?: { key: string }; - property?: string; + property: string; query: { [x: string]: any }; + value?: any; } export default function IssuesFilter(props: Props) { return ( - { + static contextTypes = { + languages: PropTypes.object.isRequired + }; + getSearchOptions = () => { - let languageKeys = Object.keys(this.props.languages); + let languageKeys = Object.keys(this.context.languages); if (this.props.facet) { languageKeys = difference(languageKeys, Object.keys(this.props.facet)); } return languageKeys .slice(0, LIST_SIZE) - .map(key => ({ label: this.props.languages[key].name, value: key })); + .map(key => ({ label: this.context.languages[key].name, value: key })); }; getSortedOptions = (facet: Facet = {}) => @@ -62,7 +63,7 @@ export default class LanguagesFilter extends React.Component { renderOption = (option: string) => ( ); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.ts b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.ts deleted file mode 100644 index 8b45c11c36a..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 LanguagesFilter from './LanguagesFilter'; -import { - getProjectsAppFacetByProperty, - getProjectsAppMaxFacetValue, - getLanguages -} from '../../../store/rootReducer'; - -const mapStateToProps = (state: any, ownProps: any) => ({ - languages: getLanguages(state), - value: ownProps.query['languages'], - facet: getProjectsAppFacetByProperty(state, 'languages'), - maxFacetValue: getProjectsAppMaxFacetValue(state) -}); -export default connect(mapStateToProps)(LanguagesFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx index 424e3f21ce1..d4f71d0f4c6 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.tsx @@ -19,13 +19,17 @@ */ import * as React from 'react'; import IssuesFilter from './IssuesFilter'; +import { Facet } from '../types'; interface Props { className?: string; + facet?: Facet; headerDetail?: React.ReactNode; isFavorite?: boolean; + maxFacetValue?: number; organization?: { key: string }; query: { [x: string]: any }; + value?: any; } export default function MaintainabilityFilter(props: Props) { diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx index 29374492c73..74d688c564d 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.tsx @@ -18,25 +18,31 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import FilterContainer from './FilterContainer'; +import Filter from './Filter'; import FilterHeader from './FilterHeader'; import { translate } from '../../../helpers/l10n'; import { getSizeRatingLabel } from '../../../helpers/ratings'; -import { Facet } from './Filter'; +import { Facet } from '../types'; export interface Props { className?: string; + facet?: Facet; isFavorite?: boolean; + maxFacetValue?: number; organization?: { key: string }; property?: string; query: { [x: string]: any }; + value?: any; } export default function NewLinesFilter(props: Props) { const { property = 'new_lines' } = props; return ( - ({ - value: ownProps.query['tags'], - facet: getProjectsAppFacetByProperty(state, 'tags'), - maxFacetValue: getProjectsAppMaxFacetValue(state) -}); -export default connect(mapStateToProps)(TagsFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx index 6f839f171dc..a21a219fe68 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/IssuesFilter-test.tsx @@ -22,7 +22,7 @@ import { shallow } from 'enzyme'; import IssuesFilter from '../IssuesFilter'; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); const renderOption = wrapper.prop('renderOption'); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx index 2669fd4f1ae..27ea25d17dd 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx @@ -33,9 +33,9 @@ const languages = { const languagesFacet = { java: 39, cs: 4, js: 1 }; it('should render the languages without the ones in the facet', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(, { + context: { languages } + }); expect(wrapper).toMatchSnapshot(); }); @@ -44,35 +44,36 @@ it('should render the languages facet with the selected languages', () => { + />, + { context: { languages } } ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('Filter').shallow()).toMatchSnapshot(); }); it('should render maximum 10 languages in the searchbox results', () => { + const manyLanguages = { + ...languages, + c: { key: 'c', name: 'c' }, + d: { key: 'd', name: 'd' }, + e: { key: 'e', name: 'e' }, + f: { key: 'f', name: 'f' }, + g: { key: 'g', name: 'g' }, + h: { key: 'h', name: 'h' }, + i: { key: 'i', name: 'i' }, + k: { key: 'k', name: 'k' }, + l: { key: 'l', name: 'l' } + }; const wrapper = shallow( + />, + { context: { languages: manyLanguages } } ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap index d475b329ae7..da8e061961c 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/CoverageFilter-test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` - diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap index 4fde9336313..2da8deedd2a 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/NewLinesFilter-test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` - + pushMetricToArray(query, property, conditions, convertCoverage) + ); + + ['duplications', 'new_duplications'].forEach(property => + pushMetricToArray(query, property, conditions, convertDuplications) + ); + + ['size', 'new_lines'].forEach(property => + pushMetricToArray(query, property, conditions, convertSize) + ); + + [ + 'reliability', + 'security', + 'maintainability', + 'new_reliability', + 'new_security', + 'new_maintainability' + ].forEach(property => pushMetricToArray(query, property, conditions, convertIssuesRating)); + + ['languages', 'tags'].forEach(property => + pushMetricToArray(query, property, conditions, convertArrayMetric) + ); + + if (query['search'] != null) { + conditions.push(`${mapPropertyToMetric('search')} = "${query['search']}"`); + } + + return conditions.join(' and '); +} + +function getAsNumericRating(value: any): number | undefined { + if (value === '' || value == null || isNaN(value)) { + return undefined; + } + const num = Number(value); + return num > 0 && num < 7 ? num : undefined; +} + +function getAsLevel(value: any): Level | undefined { + if (value === 'ERROR' || value === 'WARN' || value === 'OK') { + return value; + } + return undefined; +} + +function getAsString(value: any): string | undefined { + if (typeof value !== 'string' || !value) { + return undefined; + } + return value; +} + +function getAsStringArray(value: any): string[] | undefined { + if (typeof value !== 'string' || !value) { + return undefined; + } + return value.split(','); +} + +function getView(value: any): string | undefined { + return typeof value !== 'string' || value === 'overall' ? undefined : value; +} + +function getVisualization(value: string): string | undefined { + return VISUALIZATIONS.includes(value) ? value : undefined; +} + +function convertIssuesRating(metric: string, rating: number): string { + if (rating > 1 && rating < 5) { + return `${metric} >= ${rating}`; + } else { + return `${metric} = ${rating}`; + } +} + +function convertCoverage(metric: string, coverage: number): string { + switch (coverage) { + case 1: + return metric + ' >= 80'; + case 2: + return metric + ' < 80'; + case 3: + return metric + ' < 70'; + case 4: + return metric + ' < 50'; + case 5: + return metric + ' < 30'; + case 6: + return metric + '= NO_DATA'; + default: + return ''; + } +} + +function convertDuplications(metric: string, duplications: number): string { + switch (duplications) { + case 1: + return metric + ' < 3'; + case 2: + return metric + ' >= 3'; + case 3: + return metric + ' >= 5'; + case 4: + return metric + ' >= 10'; + case 5: + return metric + ' >= 20'; + case 6: + return metric + '= NO_DATA'; + default: + return ''; + } +} + +function convertSize(metric: string, size: number): string { + switch (size) { + case 1: + return metric + ' < 1000'; + case 2: + return metric + ' >= 1000'; + case 3: + return metric + ' >= 10000'; + case 4: + return metric + ' >= 100000'; + case 5: + return metric + ' >= 500000'; + default: + return ''; + } +} + +function mapPropertyToMetric(property?: string): string | undefined { + const map: { [property: string]: string } = { + analysis_date: 'analysisDate', + reliability: 'reliability_rating', + new_reliability: 'new_reliability_rating', + security: 'security_rating', + new_security: 'new_security_rating', + maintainability: 'sqale_rating', + new_maintainability: 'new_maintainability_rating', + coverage: 'coverage', + new_coverage: 'new_coverage', + duplications: 'duplicated_lines_density', + new_duplications: 'new_duplicated_lines_density', + size: 'ncloc', + new_lines: 'new_lines', + gate: 'alert_status', + languages: 'languages', + tags: 'tags', + search: 'query' + }; + return property && map[property]; +} + +function pushMetricToArray( + query: Query, + property: string, + conditionsArray: string[], + convertFunction: (metric: string, value: any) => string +): void { + query.foo; + const metric = mapPropertyToMetric(property); + if (query[property] != null && metric) { + conditionsArray.push(convertFunction(metric, query[property])); + } +} + +function convertArrayMetric(metric: string, items: string | string[]): string { + if (!Array.isArray(items) || items.length < 2) { + return metric + ' = ' + items; + } + return `${metric} IN (${items.join(', ')})`; +} 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 deleted file mode 100644 index 46dc1f670a1..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/store/actions.js +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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 { groupBy, uniq } from 'lodash'; -import { searchProjects, setProjectTags as apiSetProjectTags } from '../../../api/components'; -import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; -import { parseError } from '../../code/utils'; -import { receiveComponents, receiveProjectTags } from '../../../store/components/actions'; -import { receiveProjects, receiveMoreProjects } from './projectsDuck'; -import { updateState } from './stateDuck'; -import { getProjectsAppState, getComponent } from '../../../store/rootReducer'; -import { getMeasuresForProjects } from '../../../api/measures'; -import { receiveComponentsMeasures } from '../../../store/measures/actions'; -import { convertToQueryData } from './utils'; -import { receiveFavorites } from '../../../store/favorites/duck'; -import { getOrganizations } from '../../../api/organizations'; -import { receiveOrganizations } from '../../../store/organizations/duck'; -import { isDiffMetric, getPeriodValue } from '../../../helpers/measures'; - -const PAGE_SIZE = 50; -const PAGE_SIZE_VISUALIZATIONS = 99; - -const METRICS = [ - 'alert_status', - 'reliability_rating', - 'security_rating', - 'sqale_rating', - 'duplicated_lines_density', - 'coverage', - 'ncloc', - 'ncloc_language_distribution' -]; - -const LEAK_METRICS = [ - 'alert_status', - 'new_bugs', - 'new_reliability_rating', - 'new_vulnerabilities', - 'new_security_rating', - 'new_code_smells', - 'new_maintainability_rating', - 'new_coverage', - 'new_duplicated_lines_density', - 'new_lines' -]; - -const METRICS_BY_VISUALIZATION = { - risk: ['reliability_rating', 'security_rating', 'coverage', 'ncloc', 'sqale_index'], - // x, y, size, color - reliability: ['ncloc', 'reliability_remediation_effort', 'bugs', 'reliability_rating'], - security: ['ncloc', 'security_remediation_effort', 'vulnerabilities', 'security_rating'], - maintainability: ['ncloc', 'sqale_index', 'code_smells', 'sqale_rating'], - coverage: ['complexity', 'coverage', 'uncovered_lines'], - duplications: ['ncloc', 'duplicated_lines', 'duplicated_blocks'] -}; - -const FACETS = [ - 'reliability_rating', - 'security_rating', - 'sqale_rating', - 'coverage', - 'duplicated_lines_density', - 'ncloc', - 'alert_status', - 'languages', - 'tags' -]; - -const LEAK_FACETS = [ - 'new_reliability_rating', - 'new_security_rating', - 'new_maintainability_rating', - 'new_coverage', - 'new_duplicated_lines_density', - 'new_lines', - 'alert_status', - 'languages', - 'tags' -]; - -const onFail = dispatch => error => { - parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); - dispatch(updateState({ loading: false })); -}; - -const onReceiveMeasures = (dispatch, expectedProjectKeys) => response => { - const byComponentKey = groupBy(response.measures, 'component'); - - const toStore = {}; - - // fill store with empty objects for expected projects - // this is required to not have "null"s for provisioned projects - expectedProjectKeys.forEach(projectKey => (toStore[projectKey] = {})); - - Object.keys(byComponentKey).forEach(componentKey => { - const measures = {}; - byComponentKey[componentKey].forEach(measure => { - measures[measure.metric] = isDiffMetric(measure.metric) - ? getPeriodValue(measure, 1) - : measure.value; - }); - toStore[componentKey] = measures; - }); - - dispatch(receiveComponentsMeasures(toStore)); -}; - -const onReceiveOrganizations = dispatch => response => { - dispatch(receiveOrganizations(response.organizations)); -}; - -const defineMetrics = query => { - switch (query.view) { - case 'visualizations': - return METRICS_BY_VISUALIZATION[query.visualization || 'risk']; - case 'leak': - return LEAK_METRICS; - default: - return METRICS; - } -}; - -const defineFacets = query => { - if (query.view === 'leak') { - return LEAK_FACETS; - } - return FACETS; -}; - -const fetchProjectMeasures = (projects, query) => dispatch => { - if (!projects.length) { - return Promise.resolve(); - } - - const projectKeys = projects.map(project => project.key); - const metrics = defineMetrics(query); - return getMeasuresForProjects(projectKeys, metrics).then( - onReceiveMeasures(dispatch, projectKeys), - onFail(dispatch) - ); -}; - -const fetchProjectOrganizations = projects => dispatch => { - if (!projects.length) { - return Promise.resolve(); - } - - const organizationKeys = uniq(projects.map(project => project.organization)); - return getOrganizations(organizationKeys).then( - onReceiveOrganizations(dispatch), - onFail(dispatch) - ); -}; - -const handleFavorites = (dispatch, projects) => { - const toAdd = projects.filter(project => project.isFavorite); - const toRemove = projects.filter(project => project.isFavorite === false); - if (toAdd.length || toRemove.length) { - dispatch(receiveFavorites(toAdd, toRemove)); - } -}; - -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, query)), - dispatch(fetchProjectOrganizations(response.components)) - ]).then(() => { - dispatch(updateState({ loading: false })); - }); - dispatch( - updateState({ - total: response.paging.total, - pageIndex: response.paging.pageIndex - }) - ); -}; - -const onReceiveMoreProjects = (dispatch, query) => response => { - dispatch(receiveComponents(response.components)); - dispatch(receiveMoreProjects(response.components)); - handleFavorites(dispatch, response.components); - Promise.all([ - dispatch(fetchProjectMeasures(response.components, query)), - dispatch(fetchProjectOrganizations(response.components)) - ]).then(() => { - dispatch(updateState({ loading: false })); - }); - dispatch(updateState({ pageIndex: response.paging.pageIndex })); -}; - -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, - facets: defineFacets(query).join(), - f: 'analysisDate,leakPeriodDate' - }); - return searchProjects(data).then(onReceiveProjects(dispatch, query), onFail(dispatch)); -}; - -export const fetchMoreProjects = (query, isFavorite, organization) => (dispatch, getState) => { - dispatch(updateState({ loading: true })); - const state = getState(); - const { pageIndex } = getProjectsAppState(state); - const data = convertToQueryData(query, isFavorite, organization, { - ps: PAGE_SIZE, - p: pageIndex + 1, - f: 'analysisDate,leakPeriodDate' - }); - return searchProjects(data).then(onReceiveMoreProjects(dispatch, query), onFail(dispatch)); -}; - -export const setProjectTags = (project, tags) => (dispatch, getState) => { - const previousTags = getComponent(getState(), project).tags; - dispatch(receiveProjectTags(project, tags)); - return apiSetProjectTags({ project, tags: tags.join(',') }).then(null, error => { - dispatch(receiveProjectTags(project, previousTags)); - onFail(dispatch)(error); - }); -}; diff --git a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js b/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js deleted file mode 100644 index f47b8d67471..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 { flatMap, sumBy } from 'lodash'; -import { createMap } from '../../../store/utils/generalReducers'; -import { actions } from './projectsDuck'; -import { mapMetricToProperty } from './utils'; - -const CUMULATIVE_FACETS = [ - 'reliability', - 'new_reliability', - 'security', - 'new_security', - 'maintainability', - 'new_maintainability', - 'coverage', - 'new_coverage', - 'duplications', - 'new_duplications', - 'size', - 'new_lines' -]; - -const REVERSED_FACETS = ['coverage', 'new_coverage']; - -const mapFacetValues = values => { - const map = {}; - values.forEach(value => { - map[value.val] = value.count; - }); - return map; -}; - -const cumulativeMapFacetValues = values => { - const map = {}; - let sum = sumBy(values, value => value.count); - values.forEach((value, index) => { - map[value.val] = index > 0 && index < values.length - 1 ? sum : value.count; - sum -= value.count; - }); - return map; -}; - -const getFacetsMap = facets => { - const map = {}; - facets.forEach(facet => { - const property = mapMetricToProperty(facet.property); - const { values } = facet; - if (REVERSED_FACETS.includes(property)) { - values.reverse(); - } - map[property] = CUMULATIVE_FACETS.includes(property) - ? cumulativeMapFacetValues(values) - : mapFacetValues(values); - }); - return map; -}; - -const reducer = createMap( - (state, action) => action.type === actions.RECEIVE_PROJECTS, - () => false, - (state, action) => getFacetsMap(action.facets) -); - -export default reducer; - -export const getFacetByProperty = (state, property) => state[property]; - -export const getMaxFacetValue = state => { - const allValues = flatMap(Object.values(state), facet => Object.values(facet)); - return Math.max.apply(null, allValues); -}; diff --git a/server/sonar-web/src/main/js/apps/projects/store/projectsDuck.js b/server/sonar-web/src/main/js/apps/projects/store/projectsDuck.js deleted file mode 100644 index fa23ce9ea43..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/store/projectsDuck.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. - */ -export const actions = { - RECEIVE_PROJECTS: 'projects/RECEIVE_PROJECTS', - RECEIVE_MORE_PROJECTS: 'projects/RECEIVE_MORE_PROJECTS' -}; - -export const receiveProjects = (projects, facets) => ({ - type: actions.RECEIVE_PROJECTS, - projects, - facets -}); - -export const receiveMoreProjects = projects => ({ - type: actions.RECEIVE_MORE_PROJECTS, - projects -}); - -const reducer = (state = null, action = {}) => { - if (action.type === actions.RECEIVE_PROJECTS) { - return action.projects.map(project => project.key); - } - - if (action.type === actions.RECEIVE_MORE_PROJECTS) { - const keys = action.projects.map(project => project.key); - return state != null ? [...state, ...keys] : keys; - } - - return state; -}; - -export default reducer; - -export const getProjects = state => state; diff --git a/server/sonar-web/src/main/js/apps/projects/store/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/reducer.js deleted file mode 100644 index f7713f1ffd7..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/store/reducer.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { combineReducers } from 'redux'; -import projects, * as fromProjects from './projectsDuck'; -import state from './stateDuck'; -import facets, * as fromFacets from './facetsDuck'; - -export default combineReducers({ projects, state, facets }); - -export const getProjects = state => fromProjects.getProjects(state.projects); - -export const getState = state => state.state; - -export const getFacetByProperty = (state, property) => - fromFacets.getFacetByProperty(state.facets, property); - -export const getMaxFacetValue = state => fromFacets.getMaxFacetValue(state.facets); 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 deleted file mode 100644 index 190facb928c..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/store/utils.js +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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 { VISUALIZATIONS } from '../utils'; - -const getAsNumericRating = value => { - if (value === '' || value == null || isNaN(value)) { - return null; - } - const num = Number(value); - return num > 0 && num < 7 ? num : null; -}; - -const getAsLevel = value => { - if (value === 'ERROR' || value === 'WARN' || value === 'OK') { - return value; - } - return null; -}; - -// TODO Maybe use parseAsString form helpers/query -const getAsString = value => { - if (!value) { - return null; - } - return value; -}; - -// TODO Maybe move it to helpers/query -const getAsArray = (values, elementGetter) => { - if (!values) { - return null; - } - return values.split(',').map(elementGetter); -}; - -const getView = rawValue => (rawValue === 'overall' ? undefined : rawValue); - -const getVisualization = value => { - return VISUALIZATIONS.includes(value) ? value : null; -}; - -export const parseUrlQuery = urlQuery => ({ - gate: getAsLevel(urlQuery['gate']), - reliability: getAsNumericRating(urlQuery['reliability']), - new_reliability: getAsNumericRating(urlQuery['new_reliability']), - security: getAsNumericRating(urlQuery['security']), - new_security: getAsNumericRating(urlQuery['new_security']), - maintainability: getAsNumericRating(urlQuery['maintainability']), - new_maintainability: getAsNumericRating(urlQuery['new_maintainability']), - coverage: getAsNumericRating(urlQuery['coverage']), - new_coverage: getAsNumericRating(urlQuery['new_coverage']), - duplications: getAsNumericRating(urlQuery['duplications']), - new_duplications: getAsNumericRating(urlQuery['new_duplications']), - size: getAsNumericRating(urlQuery['size']), - new_lines: getAsNumericRating(urlQuery['new_lines']), - languages: getAsArray(urlQuery['languages'], getAsString), - tags: getAsArray(urlQuery['tags'], getAsString), - search: getAsString(urlQuery['search']), - sort: getAsString(urlQuery['sort']), - view: getView(urlQuery['view']), - visualization: getVisualization(urlQuery['visualization']) -}); - -export const mapMetricToProperty = metricKey => { - const map = { - analysisDate: 'analysis_date', - reliability_rating: 'reliability', - new_reliability_rating: 'new_reliability', - security_rating: 'security', - new_security_rating: 'new_security', - sqale_rating: 'maintainability', - new_maintainability_rating: 'new_maintainability', - coverage: 'coverage', - new_coverage: 'new_coverage', - duplicated_lines_density: 'duplications', - new_duplicated_lines_density: 'new_duplications', - ncloc: 'size', - new_lines: 'new_lines', - alert_status: 'gate', - languages: 'languages', - tags: 'tags', - query: 'search' - }; - return map[metricKey]; -}; - -export const mapPropertyToMetric = property => { - const map = { - analysis_date: 'analysisDate', - reliability: 'reliability_rating', - new_reliability: 'new_reliability_rating', - security: 'security_rating', - new_security: 'new_security_rating', - maintainability: 'sqale_rating', - new_maintainability: 'new_maintainability_rating', - coverage: 'coverage', - new_coverage: 'new_coverage', - duplications: 'duplicated_lines_density', - new_duplications: 'new_duplicated_lines_density', - size: 'ncloc', - new_lines: 'new_lines', - gate: 'alert_status', - languages: 'languages', - tags: 'tags', - search: 'query' - }; - return map[property]; -}; - -const convertIssuesRating = (metric, rating) => { - if (rating > 1 && rating < 5) { - return `${metric} >= ${rating}`; - } else { - return `${metric} = ${rating}`; - } -}; - -const convertCoverage = (metric, coverage) => { - switch (coverage) { - case 1: - return metric + ' >= 80'; - case 2: - return metric + ' < 80'; - case 3: - return metric + ' < 70'; - case 4: - return metric + ' < 50'; - case 5: - return metric + ' < 30'; - case 6: - return metric + '= NO_DATA'; - default: - return ''; - } -}; - -const convertDuplications = (metric, duplications) => { - switch (duplications) { - case 1: - return metric + ' < 3'; - case 2: - return metric + ' >= 3'; - case 3: - return metric + ' >= 5'; - case 4: - return metric + ' >= 10'; - case 5: - return metric + ' >= 20'; - case 6: - return metric + '= NO_DATA'; - default: - return ''; - } -}; - -const convertSize = (metric, size) => { - switch (size) { - case 1: - return metric + ' < 1000'; - case 2: - return metric + ' >= 1000'; - case 3: - return metric + ' >= 10000'; - case 4: - return metric + ' >= 100000'; - case 5: - return metric + ' >= 500000'; - default: - return ''; - } -}; - -const convertArrayMetric = (metric, items) => { - if (!Array.isArray(items) || items.length < 2) { - return metric + ' = ' + items; - } - return `${metric} IN (${items.join(', ')})`; -}; - -const pushMetricToArray = (query, property, conditionsArray, convertFunction) => { - if (query[property] != null) { - conditionsArray.push(convertFunction(mapPropertyToMetric(property), query[property])); - } -}; - -export const convertToFilter = (query, isFavorite) => { - const conditions = []; - - if (isFavorite) { - conditions.push('isFavorite'); - } - - if (query['gate'] != null) { - conditions.push(mapPropertyToMetric('gate') + ' = ' + query['gate']); - } - - ['coverage', 'new_coverage'].forEach(property => - pushMetricToArray(query, property, conditions, convertCoverage) - ); - - ['duplications', 'new_duplications'].forEach(property => - pushMetricToArray(query, property, conditions, convertDuplications) - ); - - ['size', 'new_lines'].forEach(property => - pushMetricToArray(query, property, conditions, convertSize) - ); - - [ - 'reliability', - 'security', - 'maintainability', - 'new_reliability', - 'new_security', - 'new_maintainability' - ].forEach(property => pushMetricToArray(query, property, conditions, convertIssuesRating)); - - ['languages', 'tags'].forEach(property => - pushMetricToArray(query, property, conditions, convertArrayMetric) - ); - - if (query['search'] != null) { - conditions.push(`${mapPropertyToMetric('search')} = "${query['search']}"`); - } - - return conditions.join(' and '); -}; - -export const convertToSorting = ({ sort }) => { - if (sort && sort[0] === '-') { - return { s: mapPropertyToMetric(sort.substr(1)), asc: false }; - } - return { s: mapPropertyToMetric(sort) }; -}; - -export const convertToQueryData = (query, isFavorite, organization, defaultData = {}) => { - const data = { ...defaultData }; - const filter = convertToFilter(query, isFavorite); - const sort = convertToSorting(query); - - if (filter) { - data.filter = filter; - } - if (sort.s) { - data.s = sort.s; - } - if (sort.hasOwnProperty('asc')) { - data.asc = sort.asc; - } - if (organization) { - data.organization = organization.key; - } - return data; -}; diff --git a/server/sonar-web/src/main/js/apps/projects/types.ts b/server/sonar-web/src/main/js/apps/projects/types.ts index a78e422460f..27855b610e0 100644 --- a/server/sonar-web/src/main/js/apps/projects/types.ts +++ b/server/sonar-web/src/main/js/apps/projects/types.ts @@ -18,8 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ export interface Project { + analysisDate?: string; + isFavorite?: boolean; key: string; + leakPeriodDate?: string; measures: { [key: string]: string }; name: string; - organization?: { name: string }; + organization?: { key: string; name: string }; + tags: string[]; + visibility: string; +} + +export interface Facet { + [value: string]: number; +} + +export interface Facets { + [property: string]: Facet; } diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index 6a03e7b57cf..8b99d5ee803 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -17,7 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { sumBy, uniq } from 'lodash'; import { translate } from '../../helpers/l10n'; +import { RequestData } from '../../helpers/request'; +import { getOrganizations } from '../../api/organizations'; +import { searchProjects, Facet } from '../../api/components'; +import { getMeasuresForProjects } from '../../api/measures'; +import { isDiffMetric, getPeriodValue } from '../../helpers/measures'; +import { Query, convertToFilter } from './query'; interface SortingOption { class?: string; @@ -74,6 +81,84 @@ export const VISUALIZATIONS = [ 'duplications' ]; +const PAGE_SIZE = 50; +const PAGE_SIZE_VISUALIZATIONS = 99; + +const METRICS = [ + 'alert_status', + 'reliability_rating', + 'security_rating', + 'sqale_rating', + 'duplicated_lines_density', + 'coverage', + 'ncloc', + 'ncloc_language_distribution' +]; + +const LEAK_METRICS = [ + 'alert_status', + 'new_bugs', + 'new_reliability_rating', + 'new_vulnerabilities', + 'new_security_rating', + 'new_code_smells', + 'new_maintainability_rating', + 'new_coverage', + 'new_duplicated_lines_density', + 'new_lines' +]; + +const METRICS_BY_VISUALIZATION: { [x: string]: string[] } = { + risk: ['reliability_rating', 'security_rating', 'coverage', 'ncloc', 'sqale_index'], + // x, y, size, color + reliability: ['ncloc', 'reliability_remediation_effort', 'bugs', 'reliability_rating'], + security: ['ncloc', 'security_remediation_effort', 'vulnerabilities', 'security_rating'], + maintainability: ['ncloc', 'sqale_index', 'code_smells', 'sqale_rating'], + coverage: ['complexity', 'coverage', 'uncovered_lines'], + duplications: ['ncloc', 'duplicated_lines', 'duplicated_blocks'] +}; + +const FACETS = [ + 'reliability_rating', + 'security_rating', + 'sqale_rating', + 'coverage', + 'duplicated_lines_density', + 'ncloc', + 'alert_status', + 'languages', + 'tags' +]; + +const LEAK_FACETS = [ + 'new_reliability_rating', + 'new_security_rating', + 'new_maintainability_rating', + 'new_coverage', + 'new_duplicated_lines_density', + 'new_lines', + 'alert_status', + 'languages', + 'tags' +]; + +const CUMULATIVE_FACETS = [ + 'reliability', + 'new_reliability', + 'security', + 'new_security', + 'maintainability', + 'new_maintainability', + 'coverage', + 'new_coverage', + 'duplications', + 'new_duplications', + 'size', + 'new_lines' +]; + +const REVERSED_FACETS = ['coverage', 'new_coverage']; + export function localizeSorting(sort?: string): string { return translate('projects.sort', sort || 'name'); } @@ -82,3 +167,191 @@ export function parseSorting(sort: string): { sortValue: string; sortDesc: boole const desc = sort[0] === '-'; return { sortValue: desc ? sort.substr(1) : sort, sortDesc: desc }; } + +export function fetchProjects( + query: Query, + isFavorite: boolean, + organization?: string, + pageIndex = 1 +) { + const ps = query.view === 'visualizations' ? PAGE_SIZE_VISUALIZATIONS : PAGE_SIZE; + const data = convertToQueryData(query, isFavorite, organization, { + p: pageIndex > 1 ? pageIndex : undefined, + ps, + facets: defineFacets(query).join(), + f: 'analysisDate,leakPeriodDate' + }); + return searchProjects(data).then(({ components, facets, paging }) => { + return Promise.all([ + fetchProjectMeasures(components, query), + fetchProjectOrganizations(components) + ]).then(([measures, organizations]) => { + return { + facets: getFacetsMap(facets), + projects: components + .map(component => { + const componentMeasures: { [key: string]: string } = {}; + measures.filter(measure => measure.component === component.key).forEach(measure => { + const value = isDiffMetric(measure.metric) + ? getPeriodValue(measure, 1) + : measure.value; + if (value != undefined) { + componentMeasures[measure.metric] = value; + } + }); + return { ...component, measures: componentMeasures }; + }) + .map(component => { + const organization = organizations.find(o => o.key === component.organization); + return { ...component, organization }; + }), + total: paging.total + }; + }); + }); +} + +function defineMetrics(query: Query): string[] { + switch (query.view) { + case 'visualizations': + return METRICS_BY_VISUALIZATION[query.visualization || 'risk']; + case 'leak': + return LEAK_METRICS; + default: + return METRICS; + } +} + +function defineFacets(query: Query): string[] { + if (query.view === 'leak') { + return LEAK_FACETS; + } + return FACETS; +} + +function convertToQueryData( + query: Query, + isFavorite: boolean, + organization?: string, + defaultData = {} +) { + const data: RequestData = { ...defaultData, organization }; + const filter = convertToFilter(query, isFavorite); + const sort = convertToSorting(query as any); + + if (filter) { + data.filter = filter; + } + if (sort.s) { + data.s = sort.s; + } + if (sort.hasOwnProperty('asc')) { + data.asc = sort.asc; + } + return data; +} + +function fetchProjectMeasures(projects: Array<{ key: string }>, query: Query) { + if (!projects.length) { + return Promise.resolve([]); + } + + const projectKeys = projects.map(project => project.key); + const metrics = defineMetrics(query); + return getMeasuresForProjects(projectKeys, metrics); +} + +function fetchProjectOrganizations(projects: Array<{ organization: string }>) { + if (!projects.length) { + return Promise.resolve([]); + } + + const organizations = uniq(projects.map(project => project.organization)); + return getOrganizations(organizations).then(r => r.organizations); +} + +function mapFacetValues(values: Array<{ val: string; count: number }>) { + const map: { [value: string]: number } = {}; + values.forEach(value => { + map[value.val] = value.count; + }); + return map; +} + +function cumulativeMapFacetValues(values: Array<{ val: string; count: number }>) { + const map: { [value: string]: number } = {}; + let sum = sumBy(values, value => value.count); + values.forEach((value, index) => { + map[value.val] = index > 0 && index < values.length - 1 ? sum : value.count; + sum -= value.count; + }); + return map; +} + +function getFacetsMap(facets: Facet[]) { + const map: { [property: string]: { [value: string]: number } } = {}; + facets.forEach(facet => { + const property = mapMetricToProperty(facet.property); + const { values } = facet; + if (REVERSED_FACETS.includes(property)) { + values.reverse(); + } + map[property] = CUMULATIVE_FACETS.includes(property) + ? cumulativeMapFacetValues(values) + : mapFacetValues(values); + }); + return map; +} + +function mapPropertyToMetric(property?: string) { + const map: { [property: string]: string } = { + analysis_date: 'analysisDate', + reliability: 'reliability_rating', + new_reliability: 'new_reliability_rating', + security: 'security_rating', + new_security: 'new_security_rating', + maintainability: 'sqale_rating', + new_maintainability: 'new_maintainability_rating', + coverage: 'coverage', + new_coverage: 'new_coverage', + duplications: 'duplicated_lines_density', + new_duplications: 'new_duplicated_lines_density', + size: 'ncloc', + new_lines: 'new_lines', + gate: 'alert_status', + languages: 'languages', + tags: 'tags', + search: 'query' + }; + return property && map[property]; +} + +function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } { + if (sort && sort[0] === '-') { + return { s: mapPropertyToMetric(sort.substr(1)), asc: false }; + } + return { s: mapPropertyToMetric(sort) }; +} + +function mapMetricToProperty(metricKey: string) { + const map: { [metric: string]: string } = { + analysisDate: 'analysis_date', + reliability_rating: 'reliability', + new_reliability_rating: 'new_reliability', + security_rating: 'security', + new_security_rating: 'new_security', + sqale_rating: 'maintainability', + new_maintainability_rating: 'new_maintainability', + coverage: 'coverage', + new_coverage: 'new_coverage', + duplicated_lines_density: 'duplications', + new_duplicated_lines_density: 'new_duplications', + ncloc: 'size', + new_lines: 'new_lines', + alert_status: 'gate', + languages: 'languages', + tags: 'tags', + query: 'search' + }; + return map[metricKey]; +} diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx index 32070c159b5..4443de65c32 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx @@ -30,7 +30,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; interface Props { displayOrganizations: boolean; - projects?: Project[]; + projects: Project[]; sort?: string; total?: number; visualization: string; diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.ts b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.ts deleted file mode 100644 index 3b22d353e8e..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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, - areThereCustomOrganizations -} from '../../../store/rootReducer'; - -const mapStateToProps = (state: any) => { - const projectKeys: string[] = 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, - displayOrganizations: areThereCustomOrganizations(state) - }; -}; - -export default connect(mapStateToProps)(Visualizations); diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx index dcfbc066e39..1f820a8b484 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx @@ -25,7 +25,9 @@ it('renders', () => { const project1 = { key: 'foo', measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' }, - name: 'Foo' + name: 'Foo', + tags: [], + visibility: 'public' }; expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx index 7eda7c9762f..b797a6c7ed5 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx @@ -25,7 +25,9 @@ it('renders', () => { const project1 = { key: 'foo', measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' }, - name: 'Foo' + name: 'Foo', + tags: [], + visibility: 'public' }; expect( shallow( diff --git a/server/sonar-web/src/main/js/components/controls/Favorite.js b/server/sonar-web/src/main/js/components/controls/Favorite.tsx similarity index 63% rename from server/sonar-web/src/main/js/components/controls/Favorite.js rename to server/sonar-web/src/main/js/components/controls/Favorite.tsx index 4faf4e186e6..fb43626c05c 100644 --- a/server/sonar-web/src/main/js/components/controls/Favorite.js +++ b/server/sonar-web/src/main/js/components/controls/Favorite.tsx @@ -17,28 +17,23 @@ * 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 PropTypes from 'prop-types'; +import * as React from 'react'; import FavoriteBase from './FavoriteBase'; import { addFavorite, removeFavorite } from '../../api/favorites'; -export default class Favorite extends React.PureComponent { - static propTypes = { - favorite: PropTypes.bool.isRequired, - component: PropTypes.string.isRequired, - className: PropTypes.string - }; - - render() { - const { favorite, component, ...other } = this.props; +interface Props { + className?: string; + component: string; + favorite: boolean; +} - return ( - addFavorite(component)} - removeFavorite={() => removeFavorite(component)} - /> - ); - } +export default function Favorite({ favorite, component, ...other }: Props) { + return ( + addFavorite(component)} + removeFavorite={() => removeFavorite(component)} + /> + ); } diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx similarity index 77% rename from server/sonar-web/src/main/js/components/controls/FavoriteBase.js rename to server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx index d35340dbec5..fc76186117b 100644 --- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js +++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx @@ -17,30 +17,34 @@ * 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 PropTypes from 'prop-types'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import FavoriteIcon from '../icons-components/FavoriteIcon'; -export default class FavoriteBase extends React.PureComponent { - static propTypes = { - favorite: PropTypes.bool.isRequired, - addFavorite: PropTypes.func.isRequired, - removeFavorite: PropTypes.func.isRequired, - className: PropTypes.string - }; +interface Props { + addFavorite: () => Promise; + className?: string; + favorite: boolean; + removeFavorite: () => Promise; +} + +interface State { + favorite: boolean; +} - constructor(props) { +export default class FavoriteBase extends React.PureComponent { + mounted: boolean; + + constructor(props: Props) { super(props); this.state = { favorite: this.props.favorite }; } - componentWillMount() { + componentDidMount() { this.mounted = true; - this.toggleFavorite = this.toggleFavorite.bind(this); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { if (nextProps.favorite !== this.props.favorite && nextProps.favorite !== this.state.favorite) { this.setState({ favorite: nextProps.favorite }); } @@ -50,14 +54,14 @@ export default class FavoriteBase extends React.PureComponent { this.mounted = false; } - toggleFavorite(e) { - e.preventDefault(); + toggleFavorite = (event: React.SyntheticEvent) => { + event.preventDefault(); if (this.state.favorite) { this.removeFavorite(); } else { this.addFavorite(); } - } + }; addFavorite() { this.props.addFavorite().then(() => { diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.ts b/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx similarity index 73% rename from server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.ts rename to server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx index 3d0429e6e3c..0601fc5bd29 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.ts +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx @@ -17,12 +17,10 @@ * 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 ProjectCardLanguages from './ProjectCardLanguages'; -import { getLanguages } from '../../../store/rootReducer'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Favorite from '../Favorite'; -const mapStateToProps = (state: any) => ({ - languages: getLanguages(state) +it('renders', () => { + expect(shallow()).toMatchSnapshot(); }); - -export default connect(mapStateToProps)(ProjectCardLanguages); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.tsx similarity index 96% rename from server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.js rename to server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.tsx index 86ab63dbc6c..a6231ceb7ff 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.js +++ b/server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.tsx @@ -17,17 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; import FavoriteBase from '../FavoriteBase'; import { click } from '../../../helpers/testUtils'; -function renderFavoriteBase(props) { - return shallow( - - ); -} - it('should render favorite', () => { const favorite = renderFavoriteBase({ favorite: true }); expect(favorite).toMatchSnapshot(); @@ -51,3 +45,9 @@ it('should remove favorite', () => { click(favorite.find('a')); expect(removeFavorite).toBeCalled(); }); + +function renderFavoriteBase(props?: any) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap new file mode 100644 index 00000000000..dd978f6e724 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.js.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap similarity index 92% rename from server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.js.snap rename to server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap index c7cccad562c..98dc0d71b5b 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.js.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap @@ -8,7 +8,6 @@ exports[`should render favorite 1`] = ` > `; @@ -21,7 +20,6 @@ exports[`should render not favorite 1`] = ` > `; diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts index 41dec890089..8c200a239ac 100644 --- a/server/sonar-web/src/main/js/helpers/measures.ts +++ b/server/sonar-web/src/main/js/helpers/measures.ts @@ -85,7 +85,7 @@ export function enhanceMeasuresWithMetrics( } /** Get period value of a measure */ -export function getPeriodValue(measure: Measure, periodIndex: number): string | number | undefined { +export function getPeriodValue(measure: Measure, periodIndex: number): string | undefined { const { periods } = measure; const period = periods && periods.find(period => period.index === periodIndex); return period ? period.value : undefined; diff --git a/server/sonar-web/src/main/js/store/components/actions.js b/server/sonar-web/src/main/js/store/components/actions.js deleted file mode 100644 index 873640f7092..00000000000 --- a/server/sonar-web/src/main/js/store/components/actions.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ -export const RECEIVE_COMPONENTS = 'RECEIVE_COMPONENTS'; -export const RECEIVE_PROJECT_TAGS = 'RECEIVE_PROJECT_TAGS'; - -export const receiveComponents = components => ({ - type: RECEIVE_COMPONENTS, - components -}); - -export const receiveProjectTags = (project, tags) => ({ - type: RECEIVE_PROJECT_TAGS, - project, - tags -}); diff --git a/server/sonar-web/src/main/js/store/components/reducer.js b/server/sonar-web/src/main/js/store/components/reducer.js deleted file mode 100644 index e8a96a36685..00000000000 --- a/server/sonar-web/src/main/js/store/components/reducer.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { combineReducers } from 'redux'; -import { keyBy, uniq } from 'lodash'; -import { RECEIVE_COMPONENTS, RECEIVE_PROJECT_TAGS } from './actions'; - -const byKey = (state = {}, action = {}) => { - if (action.type === RECEIVE_COMPONENTS) { - const changes = keyBy(action.components, 'key'); - return { ...state, ...changes }; - } - - if (action.type === RECEIVE_PROJECT_TAGS) { - const project = state[action.project]; - if (project) { - return { ...state, [action.project]: { ...project, tags: action.tags } }; - } - } - - return state; -}; - -const keys = (state = [], action = {}) => { - if (action.type === RECEIVE_COMPONENTS) { - const changes = action.components.map(f => f.key); - return uniq([...state, ...changes]); - } - - return state; -}; - -export default combineReducers({ byKey, keys }); - -export const getComponent = (state, key) => state.byKey[key]; diff --git a/server/sonar-web/src/main/js/store/measures/actions.js b/server/sonar-web/src/main/js/store/measures/actions.js deleted file mode 100644 index a239d303e2d..00000000000 --- a/server/sonar-web/src/main/js/store/measures/actions.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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. - */ -export const RECEIVE_COMPONENT_MEASURE = 'RECEIVE_COMPONENT_MEASURE'; - -export const receiveComponentMeasure = (componentKey, metricKey, value) => ({ - type: RECEIVE_COMPONENT_MEASURE, - componentKey, - metricKey, - value -}); - -export const RECEIVE_COMPONENT_MEASURES = 'RECEIVE_COMPONENT_MEASURES'; - -export const receiveComponentMeasures = (componentKey, measures) => ({ - type: RECEIVE_COMPONENT_MEASURES, - componentKey, - measures -}); - -export const RECEIVE_COMPONENTS_MEASURES = 'RECEIVE_COMPONENTS_MEASURES'; - -export const receiveComponentsMeasures = componentsWithMeasures => ({ - type: RECEIVE_COMPONENTS_MEASURES, - componentsWithMeasures -}); diff --git a/server/sonar-web/src/main/js/store/measures/reducer.js b/server/sonar-web/src/main/js/store/measures/reducer.js deleted file mode 100644 index 4c89ebab8ea..00000000000 --- a/server/sonar-web/src/main/js/store/measures/reducer.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 { - RECEIVE_COMPONENT_MEASURE, - RECEIVE_COMPONENT_MEASURES, - RECEIVE_COMPONENTS_MEASURES -} from './actions'; - -const byMetricKey = (state = {}, action = {}) => { - if (action.type === RECEIVE_COMPONENT_MEASURE) { - return { ...state, [action.metricKey]: action.value }; - } - - if (action.type === RECEIVE_COMPONENT_MEASURES) { - return { ...state, ...action.measures }; - } - - return state; -}; - -const reducer = (state = {}, action = {}) => { - if ([RECEIVE_COMPONENT_MEASURE, RECEIVE_COMPONENT_MEASURES].includes(action.type)) { - const component = state[action.componentKey]; - return { ...state, [action.componentKey]: byMetricKey(component, action) }; - } - - if (action.type === RECEIVE_COMPONENTS_MEASURES) { - const newState = { ...state }; - Object.keys(action.componentsWithMeasures).forEach(componentKey => { - Object.assign(newState, { - [componentKey]: byMetricKey(state[componentKey], { - type: RECEIVE_COMPONENT_MEASURES, - measures: action.componentsWithMeasures[componentKey] - }) - }); - }); - return newState; - } - - return state; -}; - -export default reducer; - -export const getComponentMeasure = (state, componentKey, metricKey) => { - const component = state[componentKey]; - return component && component[metricKey]; -}; - -export const getComponentMeasures = (state, componentKey) => state[componentKey]; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index ef3e95d1931..7ab8741e79f 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -19,11 +19,9 @@ */ import { combineReducers } from 'redux'; import appState from './appState/duck'; -import components, * as fromComponents from './components/reducer'; import users, * as fromUsers from './users/reducer'; import favorites, * as fromFavorites from './favorites/duck'; import languages, * as fromLanguages from './languages/reducer'; -import measures, * as fromMeasures from './measures/reducer'; import metrics, * as fromMetrics from './metrics/reducer'; import notifications, * as fromNotifications from './notifications/duck'; import organizations, * as fromOrganizations from './organizations/duck'; @@ -31,17 +29,14 @@ import organizationsMembers, * as fromOrganizationsMembers from './organizations import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer'; import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer'; -import projectsApp, * as fromProjectsApp from '../apps/projects/store/reducer'; import qualityGatesApp from '../apps/quality-gates/store/rootReducer'; import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer'; export default combineReducers({ appState, - components, globalMessages, favorites, languages, - measures, metrics, notifications, organizations, @@ -51,15 +46,12 @@ export default combineReducers({ // apps permissionsApp, projectAdminApp, - projectsApp, qualityGatesApp, settingsApp }); export const getAppState = state => state.appState; -export const getComponent = (state, key) => fromComponents.getComponent(state.components, key); - export const getGlobalMessages = state => fromGlobalMessages.getGlobalMessages(state.globalMessages); @@ -81,12 +73,6 @@ export const getUsers = state => fromUsers.getUsers(state.users); export const isFavorite = (state, componentKey) => fromFavorites.isFavorite(state.favorites, componentKey); -export const getComponentMeasure = (state, componentKey, metricKey) => - fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey); - -export const getComponentMeasures = (state, componentKey) => - fromMeasures.getComponentMeasures(state.measures, componentKey); - export const getMetrics = state => fromMetrics.getMetrics(state.metrics); export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key); @@ -126,16 +112,6 @@ export const getOrganizationMembersLogins = (state, organization) => export const getOrganizationMembersState = (state, organization) => fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization); -export const getProjects = state => fromProjectsApp.getProjects(state.projectsApp); - -export const getProjectsAppState = state => fromProjectsApp.getState(state.projectsApp); - -export const getProjectsAppFacetByProperty = (state, property) => - fromProjectsApp.getFacetByProperty(state.projectsApp, property); - -export const getProjectsAppMaxFacetValue = state => - fromProjectsApp.getMaxFacetValue(state.projectsApp); - export const getQualityGatesAppState = state => state.qualityGatesApp; export const getPermissionsAppUsers = state => fromPermissionsApp.getUsers(state.permissionsApp); -- 2.39.5