diff options
Diffstat (limited to 'server/sonar-web')
76 files changed, 2784 insertions, 109 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index e44e6d19da4..9577d494429 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -59,6 +59,7 @@ "react-router-redux": "4.0.2", "react-select": "1.0.0-beta12", "react-transform-hmr": "1.0.4", + "react-virtualized": "8.1.1", "redux": "3.3.1", "redux-logger": "2.2.1", "redux-simple-router": "1.0.1", diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index 6b3238022fb..ac46a7680f1 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -110,6 +110,11 @@ export function getMyProjects (data) { return getJSON(url, data); } +export function searchProjects (data) { + const url = '/api/components/search_projects'; + return getJSON(url, data); +} + /** * Change component's key * @param {string} key diff --git a/server/sonar-web/src/main/js/api/measures.js b/server/sonar-web/src/main/js/api/measures.js index 0b6bd6b4716..f04d579bf08 100644 --- a/server/sonar-web/src/main/js/api/measures.js +++ b/server/sonar-web/src/main/js/api/measures.js @@ -33,3 +33,10 @@ export function getMeasuresAndMeta (componentKey, metrics, additional = {}) { }); return getJSON(url, data); } + +export const getMeasuresForComponents = (componentKeys, metricKeys) => ( + getJSON('/api/measures/search', { + componentKeys: componentKeys.join(), + metricKeys: metricKeys.join() + }) +); diff --git a/server/sonar-web/src/main/js/app/components/App.js b/server/sonar-web/src/main/js/app/components/App.js index b4fac8bf062..c630a4fe275 100644 --- a/server/sonar-web/src/main/js/app/components/App.js +++ b/server/sonar-web/src/main/js/app/components/App.js @@ -20,6 +20,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { fetchCurrentUser } from '../store/users/actions'; +import { fetchLanguages } from '../store/rootActions'; class App extends React.Component { static propTypes = { @@ -28,6 +29,7 @@ class App extends React.Component { componentDidMount () { this.props.fetchCurrentUser(); + this.props.fetchLanguages(); } render () { @@ -37,5 +39,5 @@ class App extends React.Component { export default connect( () => ({}), - { fetchCurrentUser } + { fetchCurrentUser, fetchLanguages } )(App); diff --git a/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js new file mode 100644 index 00000000000..0ee6c22edb5 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js @@ -0,0 +1,28 @@ +/* + * 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 { connect } from 'react-redux'; +import GlobalMessages from '../../components/controls/GlobalMessages'; +import { getGlobalMessages } from '../store/rootReducer'; + +const mapStateToProps = state => ({ + messages: getGlobalMessages(state) +}); + +export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/app/index.js b/server/sonar-web/src/main/js/app/index.js index 35a9525f137..addf577ee07 100644 --- a/server/sonar-web/src/main/js/app/index.js +++ b/server/sonar-web/src/main/js/app/index.js @@ -24,9 +24,10 @@ import { createHistory } from 'history'; import { Provider } from 'react-redux'; import App from './components/App'; import accountRoutes from '../apps/account/routes'; +import projectsRouters from '../apps/projects/routes'; import configureStore from '../components/store/configureStore'; import rootReducer from './store/rootReducer'; -import './styles.css'; +import './styles/index'; window.sonarqube.appStarted.then(options => { const el = document.querySelector(options.el); @@ -42,6 +43,7 @@ window.sonarqube.appStarted.then(options => { <Router history={history}> <Route path="/" component={App}> {accountRoutes} + {projectsRouters} </Route> </Router> </Provider> diff --git a/server/sonar-web/src/main/js/app/store/components/actions.js b/server/sonar-web/src/main/js/app/store/components/actions.js new file mode 100644 index 00000000000..3ba34eb2eb8 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/components/actions.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +export const RECEIVE_COMPONENTS = 'RECEIVE_COMPONENTS'; + +export const receiveComponents = components => ({ + type: RECEIVE_COMPONENTS, + components +}); diff --git a/server/sonar-web/src/main/js/app/store/components/reducer.js b/server/sonar-web/src/main/js/app/store/components/reducer.js new file mode 100644 index 00000000000..c2d609a1066 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/components/reducer.js @@ -0,0 +1,47 @@ +/* + * 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 { combineReducers } from 'redux'; +import keyBy from 'lodash/keyBy'; +import uniq from 'lodash/uniq'; +import { RECEIVE_COMPONENTS } from './actions'; + +const byKey = (state = {}, action = {}) => { + if (action.type === RECEIVE_COMPONENTS) { + const changes = keyBy(action.components, 'key'); + return { ...state, ...changes }; + } + + 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/app/store/languages/actions.js b/server/sonar-web/src/main/js/app/store/languages/actions.js new file mode 100644 index 00000000000..9d2d517acc2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/languages/actions.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +export const RECEIVE_LANGUAGES = 'RECEIVE_LANGUAGES'; + +export const receiveLanguages = languages => ({ + type: RECEIVE_LANGUAGES, + languages +}); diff --git a/server/sonar-web/src/main/js/app/store/languages/reducer.js b/server/sonar-web/src/main/js/app/store/languages/reducer.js new file mode 100644 index 00000000000..73eb9d5f037 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/languages/reducer.js @@ -0,0 +1,35 @@ +/* + * 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 keyBy from 'lodash/keyBy'; +import { RECEIVE_LANGUAGES } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === RECEIVE_LANGUAGES) { + return keyBy(action.languages, 'key'); + } + + return state; +}; + +export default reducer; + +export const getLanguages = state => ( + state +); diff --git a/server/sonar-web/src/main/js/app/store/measures/actions.js b/server/sonar-web/src/main/js/app/store/measures/actions.js index 7445f7fae44..2321163afd0 100644 --- a/server/sonar-web/src/main/js/app/store/measures/actions.js +++ b/server/sonar-web/src/main/js/app/store/measures/actions.js @@ -25,3 +25,18 @@ export const receiveComponentMeasure = (componentKey, metricKey, value) => ({ 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/app/store/measures/reducer.js b/server/sonar-web/src/main/js/app/store/measures/reducer.js index 5197ebf8dd5..3d166663836 100644 --- a/server/sonar-web/src/main/js/app/store/measures/reducer.js +++ b/server/sonar-web/src/main/js/app/store/measures/reducer.js @@ -17,22 +17,39 @@ * 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 } from './actions'; +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 (action.type === RECEIVE_COMPONENT_MEASURE) { + 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; }; @@ -42,3 +59,7 @@ 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/app/store/rootActions.js b/server/sonar-web/src/main/js/app/store/rootActions.js new file mode 100644 index 00000000000..92f6f4734cf --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/rootActions.js @@ -0,0 +1,34 @@ +/* + * 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 { getLanguages } from '../../api/languages'; +import { receiveLanguages } from './languages/actions'; +import { addGlobalErrorMessage } from '../../components/store/globalMessages'; +import { parseError } from '../../apps/code/utils'; + +const onFail = dispatch => error => { + parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); +}; + +export const fetchLanguages = () => dispatch => { + return getLanguages().then( + languages => dispatch(receiveLanguages(languages)), + onFail(dispatch) + ); +}; diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js index de0ad66b3f9..102ae24d908 100644 --- a/server/sonar-web/src/main/js/app/store/rootReducer.js +++ b/server/sonar-web/src/main/js/app/store/rootReducer.js @@ -18,13 +18,40 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { combineReducers } from 'redux'; +import components, * as fromComponents from './components/reducer'; import users, * as fromUsers from './users/reducer'; import favorites, * as fromFavorites from './favorites/reducer'; +import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; +import globalMessages, * as fromGlobalMessages from '../../components/store/globalMessages'; import issuesActivity, * as fromIssuesActivity from '../../apps/account/home/store/reducer'; +import projectsApp, * as fromProjectsApp from '../../apps/projects/store/reducer'; -export default combineReducers({ users, favorites, issuesActivity, measures }); +export default combineReducers({ + components, + globalMessages, + favorites, + languages, + measures, + users, + + // apps + issuesActivity, + projectsApp +}); + +export const getComponent = (state, key) => ( + fromComponents.getComponent(state.components, key) +); + +export const getGlobalMessages = state => ( + fromGlobalMessages.getGlobalMessages(state.globalMessages) +); + +export const getLanguages = (state, key) => ( + fromLanguages.getLanguages(state.languages, key) +); export const getCurrentUser = state => ( fromUsers.getCurrentUser(state.users) @@ -41,3 +68,19 @@ export const getIssuesActivity = state => ( export const getComponentMeasure = (state, componentKey, metricKey) => ( fromMeasures.getComponentMeasure(state.measures, componentKey, metricKey) ); + +export const getComponentMeasures = (state, componentKey) => ( + fromMeasures.getComponentMeasures(state.measures, componentKey) +); + +export const getProjects = state => ( + fromProjectsApp.getProjects(state.projectsApp) +); + +export const getProjectsAppState = state => ( + fromProjectsApp.getState(state.projectsApp) +); + +export const getProjectsAppFilterStatus = (state, key) => ( + fromProjectsApp.getFilterStatus(state.projectsApp, key) +); diff --git a/server/sonar-web/src/main/js/app/styles.css b/server/sonar-web/src/main/js/app/styles/boxed-group.css index 1fa93c779ad..1fa93c779ad 100644 --- a/server/sonar-web/src/main/js/app/styles.css +++ b/server/sonar-web/src/main/js/app/styles/boxed-group.css diff --git a/server/sonar-web/src/main/js/app/styles/index.js b/server/sonar-web/src/main/js/app/styles/index.js new file mode 100644 index 00000000000..0f7e68f8ed3 --- /dev/null +++ b/server/sonar-web/src/main/js/app/styles/index.js @@ -0,0 +1,21 @@ +/* + * 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 './boxed-group.css'; +import './page.css'; diff --git a/server/sonar-web/src/main/js/app/styles/page.css b/server/sonar-web/src/main/js/app/styles/page.css new file mode 100644 index 00000000000..c31fc2375ad --- /dev/null +++ b/server/sonar-web/src/main/js/app/styles/page.css @@ -0,0 +1,26 @@ +.page-head { + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #e6e6e6; + background-color: #f3f3f3; +} + +.page-head .page-actions { + margin-bottom: 0; +} + +.page-head .spinner { + top: 4px; + margin-left: 8px; +} + +.page-with-left-sidebar .page-main { + order: 2; +} + +.page-with-left-sidebar .page-sidebar, +.page-with-left-sidebar .page-sidebar-fixed { + order: 1; + padding-left: 0; + padding-right: 40px; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js b/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js index 1eda2c9959f..0d9aaf2b226 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Measure.js @@ -25,6 +25,11 @@ import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { formatLeak, isDiffMetric, getRatingTooltip } from '../utils'; export default class Measure extends React.Component { + static propTypes = { + measure: React.PropTypes.object, + metric: React.PropTypes.object + }; + renderRating (measure, metric) { const value = isDiffMetric(metric) ? measure.leak : measure.value; const tooltip = getRatingTooltip(metric.key, value); diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css index 4c695a7a343..de6130fc631 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/styles.css +++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css @@ -54,6 +54,16 @@ font-weight: 300; } +.main-domain-measures .domain-measures-value .rating, +.measure-details-value .rating { + vertical-align: top; + width: 40px; + height: 40px; + line-height: 40px; + border-radius: 40px; + font-size: 24px; +} + .main-domain-measures .domain-measures-name { margin-top: 8px; } diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index 00a6df79e93..1b0b294d046 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -146,7 +146,7 @@ export default function enhance (ComposedComponent) { <div className="overview-domain-measure-sup" title={title} data-toggle="tooltip"> - <DrilldownLink component={component.key} metric={metricKey}> + <DrilldownLink className="link-no-underline" component={component.key} metric={metricKey}> <Rating value={measure.value}/> </DrilldownLink> </div> diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js index 6217905ae83..219a58d0895 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -23,6 +23,7 @@ import QualityGateConditions from './QualityGateConditions'; import EmptyQualityGate from './EmptyQualityGate'; import { ComponentType, MeasuresListType, PeriodsListType } from '../propTypes'; import { translate } from '../../../helpers/l10n'; +import Level from '../../../components/ui/Level'; function parseQualityGateDetails (rawDetails) { return JSON.parse(rawDetails); @@ -53,9 +54,7 @@ const QualityGate = ({ component, measures, periods }) => { <div className="overview-quality-gate" id="overview-quality-gate"> <h2 className="overview-title"> {translate('overview.quality_gate')} - <span className={'badge badge-' + level.toLowerCase()}> - {translate('overview.gate', level)} - </span> + <Level level={level}/> </h2> {conditions.length > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 38381fac6d5..5dc04da6d71 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -16,13 +16,8 @@ font-weight: 400; } -.overview-title > .badge { - position: relative; - top: -2px; +.overview-title > .level { margin-left: 15px; - padding: 6px 12px; - font-size: 14px; - letter-spacing: 0.05em; } .overview-title > a { diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js new file mode 100644 index 00000000000..b6abec6f45f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/App.js @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; +import Helmet from 'react-helmet'; +import PageHeaderContainer from './PageHeaderContainer'; +import ProjectsListContainer from './ProjectsListContainer'; +import ProjectsListFooterContainer from './ProjectsListFooterContainer'; +import PageSidebar from './PageSidebar'; +import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer'; +import { parseUrlQuery } from '../store/utils'; +import '../styles.css'; +import { translate } from '../../../helpers/l10n'; + +export default class App extends React.Component { + static propTypes = { + fetchProjects: React.PropTypes.func.isRequired + }; + + state = { + query: {} + }; + + componentDidMount () { + document.querySelector('html').classList.add('dashboard-page'); + this.handleQueryChange(); + } + + componentDidUpdate (prevProps) { + if (prevProps.location.query !== this.props.location.query) { + this.handleQueryChange(); + } + } + + componentWillUnmount () { + document.querySelector('html').classList.remove('dashboard-page'); + } + + handleQueryChange () { + const query = parseUrlQuery(this.props.location.query); + this.setState({ query }); + this.props.fetchProjects(query); + } + + render () { + return ( + <div id="projects-page" className="page page-limited"> + <Helmet title={translate('projects.page')} titleTemplate="SonarQube - %s"/> + + <PageHeaderContainer/> + + <GlobalMessagesContainer/> + + <div className="page-with-sidebar page-with-left-sidebar"> + <div className="page-main"> + <ProjectsListContainer/> + <ProjectsListFooterContainer query={this.state.query}/> + </div> + <aside className="page-sidebar-fixed"> + <PageSidebar query={this.state.query}/> + </aside> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/AppContainer.js b/server/sonar-web/src/main/js/apps/projects/components/AppContainer.js new file mode 100644 index 00000000000..f2956e0b14a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/AppContainer.js @@ -0,0 +1,27 @@ +/* + * 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 { connect } from 'react-redux'; +import App from './App'; +import { fetchProjects } from '../store/actions'; + +export default connect( + () => ({}), + { fetchProjects } +)(App); diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js new file mode 100644 index 00000000000..4043e0176f9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js @@ -0,0 +1,52 @@ +/* + * 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 React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class PageHeader extends React.Component { + static propTypes = { + total: React.PropTypes.number, + loading: React.PropTypes.bool + }; + + render () { + const { total, loading } = this.props; + + return ( + <header className="page-header"> + <h1 className="page-title">{translate('projects.page')}</h1> + + {!!loading && ( + <i className="spinner"/> + )} + + <div className="page-actions"> + {total != null && ( + <span><strong>{total}</strong> {translate('projects._projects')}</span> + )} + </div> + + <div className="page-description"> + {translate('projects.page.description')} + </div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js new file mode 100644 index 00000000000..fe8c6fd6043 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js @@ -0,0 +1,26 @@ +/* + * 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 { connect } from 'react-redux'; +import PageHeader from './PageHeader'; +import { getProjectsAppState } from '../../../app/store/rootReducer'; + +export default connect( + state => getProjectsAppState(state) +)(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js new file mode 100644 index 00000000000..c250d97c141 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import CoverageFilterContainer from '../filters/CoverageFilterContainer'; +import DuplicationsFilterContainer from '../filters/DuplicationsFilterContainer'; +import SizeFilterContainer from '../filters/SizeFilterContainer'; +import QualityGateFilterContainer from '../filters/QualityGateFilterContainer'; + +export default class PageSidebar extends React.Component { + render () { + return ( + <div> + <CoverageFilterContainer query={this.props.query}/> + <DuplicationsFilterContainer query={this.props.query}/> + <SizeFilterContainer query={this.props.query}/> + <QualityGateFilterContainer query={this.props.query}/> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js new file mode 100644 index 00000000000..e76fa0e975d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import ProjectCardMeasures from './ProjectCardMeasures'; +import { getComponentUrl } from '../../../helpers/urls'; + +export default class ProjectCard extends React.Component { + static propTypes = { + project: React.PropTypes.object + }; + + render () { + const { project } = this.props; + + if (project == null) { + return null; + } + + return ( + <div className="boxed-group project-card"> + <h2 className="project-card-name"> + <a className="link-base-color" href={getComponentUrl(project.key)}>{project.name}</a> + </h2> + <div className="boxed-group-inner"> + <ProjectCardMeasures measures={this.props.measures}/> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.js new file mode 100644 index 00000000000..f3f9713ffb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.js @@ -0,0 +1,29 @@ +/* + * 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 { connect } from 'react-redux'; +import ProjectCard from './ProjectCard'; +import { getComponent, getComponentMeasures } from '../../../app/store/rootReducer'; + +export default connect( + (state, ownProps) => ({ + project: getComponent(state, ownProps.projectKey), + measures: getComponentMeasures(state, ownProps.projectKey), + }) +)(ProjectCard); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.js new file mode 100644 index 00000000000..f91571ab8c3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.js @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import sortBy from 'lodash/sortBy'; +import { connect } from 'react-redux'; +import { getLanguages } from '../../../app/store/rootReducer'; +import { translate } from '../../../helpers/l10n'; + +class ProjectCardLanguages extends React.Component { + getLanguageName (key) { + if (key === '<null>') { + return translate('unknown'); + } + const language = this.props.languages[key]; + return language != null ? language.name : key; + } + + render () { + const { distribution } = this.props; + + if (distribution == null) { + return null; + } + + const parsedLanguages = distribution.split(';').map(item => item.split('=')); + const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])) + .slice(0, 2) + .map(l => this.getLanguageName(l[0])); + + return <span>{finalLanguages.join(', ')}</span>; + } +} + +const mapStateToProps = state => ({ + languages: getLanguages(state) +}); + +export default connect(mapStateToProps)(ProjectCardLanguages); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardMeasures.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardMeasures.js new file mode 100644 index 00000000000..9c7cc99abfd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardMeasures.js @@ -0,0 +1,129 @@ +/* + * 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 React from 'react'; +import ProjectCardLanguages from './ProjectCardLanguages'; +import ProjectCardQualityGate from './ProjectCardQualityGate'; +import Measure from '../../component-measures/components/Measure'; +import Rating from '../../../components/ui/Rating'; +import CoverageRating from '../../../components/ui/CoverageRating'; +import DuplicationsRating from '../../../components/ui/DuplicationsRating'; +import SizeRating from '../../../components/ui/SizeRating'; +import { translate } from '../../../helpers/l10n'; + +export default class ProjectCardMeasures extends React.Component { + static propTypes = { + measures: React.PropTypes.object, + languages: React.PropTypes.array + }; + + render () { + const { measures } = this.props; + + if (measures == null) { + return null; + } + + return ( + <div className="project-card-measures"> + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + <Rating value={measures['reliability_rating']}/> + </div> + <div className="project-card-measure-label"> + {translate('metric_domain.Reliability')} + </div> + </div> + </div> + + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + <Rating value={measures['security_rating']}/> + </div> + <div className="project-card-measure-label"> + {translate('metric_domain.Security')} + </div> + </div> + </div> + + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + <Rating value={measures['sqale_rating']}/> + </div> + <div className="project-card-measure-label"> + {translate('metric_domain.Maintainability')} + </div> + </div> + </div> + + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + {measures['coverage'] != null && ( + <span className="spacer-right"> + <CoverageRating value={measures['coverage']}/> + </span> + )} + <Measure measure={{ value: measures['coverage'] }} + metric={{ key: 'coverage', type: 'PERCENT' }}/> + </div> + <div className="project-card-measure-label"> + {translate('metric.coverage.name')} + </div> + </div> + </div> + + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + <span className="spacer-right"> + <DuplicationsRating value={measures['duplicated_lines_density']}/> + </span> + <Measure measure={{ value: measures['duplicated_lines_density'] }} + metric={{ key: 'duplicated_lines_density', type: 'PERCENT' }}/> + </div> + <div className="project-card-measure-label"> + {translate('metric.duplicated_lines_density.short_name')} + </div> + </div> + </div> + + <div className="project-card-measure"> + <div className="project-card-measure-inner"> + <div className="project-card-measure-number"> + <span className="spacer-right"> + <SizeRating value={measures['ncloc']}/> + </span> + <Measure measure={{ value: measures['ncloc'] }} + metric={{ key: 'ncloc', type: 'SHORT_INT' }}/> + </div> + <div className="project-card-measure-label"> + <ProjectCardLanguages distribution={measures['ncloc_language_distribution']}/> + </div> + </div> + </div> + + <ProjectCardQualityGate status={measures['alert_status']}/> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js new file mode 100644 index 00000000000..cc542cabefa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import Level from '../../../components/ui/Level'; +import { translate } from '../../../helpers/l10n'; + +export default class ProjectCardQualityGate extends React.Component { + static propTypes = { + status: React.PropTypes.string + }; + + render () { + const { status } = this.props; + + if (!status) { + return null; + } + + return ( + <div className="project-card-measure pull-right"> + <div className="project-card-measure-inner"> + <div> + <Level level={status}/> + </div> + <div className="project-card-measure-label"> + {translate('overview.quality_gate')} + </div> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js new file mode 100644 index 00000000000..47f4a105589 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js @@ -0,0 +1,75 @@ +/* + * 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 React from 'react'; +import { List, AutoSizer, WindowScroller } from 'react-virtualized'; +import ProjectCardContainer from './ProjectCardContainer'; +import { translate } from '../../../helpers/l10n'; + +export default class ProjectsList extends React.Component { + static propTypes = { + projects: React.PropTypes.arrayOf(React.PropTypes.string) + }; + + render () { + const { projects } = this.props; + + if (projects == null) { + return null; + } + + if (projects.length === 0) { + return ( + <div className="projects-empty-list"> + <h3>{translate('projects.no_projects.1')}</h3> + <p className="big-spacer-top">{translate('projects.no_projects.2')}</p> + </div> + ); + } + + const rowRenderer = ({ key, index, style }) => { + const projectKey = projects[index]; + return ( + <div key={key} style={style}> + <ProjectCardContainer projectKey={projectKey}/> + </div> + ); + }; + + return ( + <WindowScroller> + {({ height, scrollTop }) => ( + <AutoSizer disableHeight> + {({ width }) => ( + <List + className="projects-list" + autoHeight + width={width} + height={height} + rowCount={projects.length} + rowHeight={135} + rowRenderer={rowRenderer} + scrollTop={scrollTop}/> + )} + </AutoSizer> + )} + </WindowScroller> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.js new file mode 100644 index 00000000000..bdeb573a65a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.js @@ -0,0 +1,28 @@ +/* + * 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 { connect } from 'react-redux'; +import ProjectsList from './ProjectsList'; +import { getProjects } from '../../../app/store/rootReducer'; + +export default connect( + state => ({ + projects: getProjects(state) + }) +)(ProjectsList); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooter.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooter.js new file mode 100644 index 00000000000..d256492d68e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooter.js @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import ListFooter from '../../../components/controls/ListFooter'; + +export default class ProjectsListFooter extends React.Component { + static propTypes = { + total: React.PropTypes.number.isRequired, + }; + + render () { + if (!this.props.total) { + return null; + } + + return <ListFooter {...this.props}/>; + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.js new file mode 100644 index 00000000000..d30f699c723 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.js @@ -0,0 +1,42 @@ +/* + * 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 { connect } from 'react-redux'; +import { getProjects, getProjectsAppState } from '../../../app/store/rootReducer'; +import { fetchMoreProjects } from '../store/actions'; +import ProjectsListFooter from './ProjectsListFooter'; + +const mapStateToProps = state => { + 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, ownProps) => ({ + loadMore: () => dispatch(fetchMoreProjects(ownProps.query)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ProjectsListFooter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js new file mode 100644 index 00000000000..b6bab069112 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js @@ -0,0 +1,126 @@ +/* + * 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 React from 'react'; +import { Link } from 'react-router'; +import Filter from './Filter'; +import CoverageRating from '../../../components/ui/CoverageRating'; +import { translate } from '../../../helpers/l10n'; + +export default class CoverageFilter extends React.Component { + static propTypes = { + value: React.PropTypes.shape({ + from: React.PropTypes.number, + to: React.PropTypes.number + }), + getFilterUrl: React.PropTypes.func.isRequired, + toggleFilter: React.PropTypes.func.isRequired + }; + + isOptionAction (from, to) { + const { value } = this.props; + + if (value == null) { + return false; + } + + return value.from === from && value.to === to; + } + + renderLabel (value) { + let label; + if (value.to == null) { + label = '>' + value.from; + } else if (value.from == null) { + label = '<' + value.to; + } else { + label = value.from + '–' + value.to; + } + return label + '%'; + } + + renderValue () { + const { value } = this.props; + + let average; + if (value.to == null) { + average = value.from; + } else if (value.from == null) { + average = value.to / 2; + } else { + average = (value.from + value.to) / 2; + } + + const label = this.renderLabel(value); + + return ( + <div className="projects-filter-value"> + <CoverageRating value={average}/> + + <div className="projects-filter-hint note"> + {label} + </div> + </div> + ); + } + + renderOptions () { + const options = [ + [null, 30, 15], + [30, 50, 40], + [50, 70, 60], + [70, 80, 75], + [80, null, 90], + ]; + + return ( + <div> + {options.map(option => ( + <Link key={option[2]} + className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} + to={this.props.getFilterUrl({ 'coverage__gte': option[0], 'coverage__lt': option[1] })} + onClick={this.props.toggleFilter}> + <CoverageRating value={option[2]}/> + <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> + </Link> + ))} + {this.props.value != null && ( + <div> + <hr/> + <Link className="text-center" + to={this.props.getFilterUrl({ 'coverage__gte': null, 'coverage__lt': null })} + onClick={this.props.toggleFilter}> + <span className="text-danger">{translate('reset_verb')}</span> + </Link> + </div> + )} + </div> + ); + } + + render () { + return ( + <Filter + renderName={() => 'Coverage'} + renderOptions={() => this.renderOptions()} + renderValue={() => this.renderValue()} + {...this.props}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js new file mode 100644 index 00000000000..ed2a55e8b04 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js @@ -0,0 +1,29 @@ +/* + * 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 CoverageFilter from './CoverageFilter'; +import connectFilter from './connectFilter'; + +const getValue = query => { + const from = query['coverage__gte']; + const to = query['coverage__lt']; + return from == null && to == null ? null : { from, to }; +}; + +export default connectFilter('coverage', getValue)(CoverageFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js new file mode 100644 index 00000000000..b44b437133b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js @@ -0,0 +1,126 @@ +/* + * 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 React from 'react'; +import { Link } from 'react-router'; +import Filter from './Filter'; +import DuplicationsRating from '../../../components/ui/DuplicationsRating'; +import { translate } from '../../../helpers/l10n'; + +export default class DuplicationsFilter extends React.Component { + static propTypes = { + value: React.PropTypes.shape({ + from: React.PropTypes.number, + to: React.PropTypes.number + }), + getFilterUrl: React.PropTypes.func.isRequired, + toggleFilter: React.PropTypes.func.isRequired + }; + + isOptionAction (from, to) { + const { value } = this.props; + + if (value == null) { + return false; + } + + return value.from === from && value.to === to; + } + + renderLabel (value) { + let label; + if (value.to == null) { + label = '>' + value.from; + } else if (value.from == null) { + label = '<' + value.to; + } else { + label = value.from + '–' + value.to; + } + return label + '%'; + } + + renderValue () { + const { value } = this.props; + + let average; + if (value.to == null) { + average = value.from; + } else if (value.from == null) { + average = value.to / 2; + } else { + average = (value.from + value.to) / 2; + } + + const label = this.renderLabel(value); + + return ( + <div className="projects-filter-value"> + <DuplicationsRating value={average}/> + + <div className="projects-filter-hint note"> + {label} + </div> + </div> + ); + } + + renderOptions () { + const options = [ + [null, 3, 1.5], + [3, 5, 4], + [5, 10, 7.5], + [10, 20, 15], + [20, null, 30], + ]; + + return ( + <div> + {options.map(option => ( + <Link key={option[2]} + className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} + to={this.props.getFilterUrl({ 'duplications__gte': option[0], 'duplications__lt': option[1] })} + onClick={this.props.toggleFilter}> + <DuplicationsRating value={option[2]}/> + <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> + </Link> + ))} + {this.props.value != null && ( + <div> + <hr/> + <Link className="text-center" + to={this.props.getFilterUrl({ 'duplications__gte': null, 'duplications__lt': null })} + onClick={this.props.toggleFilter}> + <span className="text-danger">{translate('reset_verb')}</span> + </Link> + </div> + )} + </div> + ); + } + + render () { + return ( + <Filter + renderName={() => 'Duplications'} + renderOptions={() => this.renderOptions()} + renderValue={() => this.renderValue()} + {...this.props}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js new file mode 100644 index 00000000000..eae923e3766 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js @@ -0,0 +1,29 @@ +/* + * 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 DuplicationsFilter from './DuplicationsFilter'; +import connectFilter from './connectFilter'; + +const getValue = query => { + const from = query['duplications__gte']; + const to = query['duplications__lt']; + return from == null && to == null ? null : { from, to }; +}; + +export default connectFilter('duplications', getValue)(DuplicationsFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.js b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js new file mode 100644 index 00000000000..5295b5628c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js @@ -0,0 +1,70 @@ +/* + * 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 React from 'react'; +import classNames from 'classnames'; + +export default class Filter extends React.Component { + static propTypes = { + getFilterUrl: React.PropTypes.func.isRequired, + isOpen: React.PropTypes.bool.isRequired, + renderName: React.PropTypes.func.isRequired, + renderOptions: React.PropTypes.func.isRequired, + renderValue: React.PropTypes.func.isRequired, + toggleFilter: React.PropTypes.func.isRequired, + value: React.PropTypes.any + }; + + handleClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.toggleFilter(); + } + + render () { + const { value, isOpen } = this.props; + const { renderName, renderOptions, renderValue } = this.props; + const className = classNames('projects-filter', { + 'projects-filter-active': value != null, + 'projects-filter-open': isOpen + }); + + return ( + <div className={className}> + <a className="projects-filter-header clearfix" href="#" onClick={e => this.handleClick(e)}> + <div className="projects-filter-name"> + {renderName()} + {' '} + {!isOpen && ( + <i className="icon-dropdown"/> + )} + </div> + + {value != null && renderValue()} + </a> + + {isOpen && ( + <div className="projects-filter-options"> + {renderOptions()} + </div> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js new file mode 100644 index 00000000000..91b58140155 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js @@ -0,0 +1,76 @@ +/* + * 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 React from 'react'; +import { Link } from 'react-router'; +import Filter from './Filter'; +import Level from '../../../components/ui/Level'; +import { translate } from '../../../helpers/l10n'; + +export default class QualityGateFilter extends React.Component { + static propTypes = { + value: React.PropTypes.any, + getFilterUrl: React.PropTypes.func.isRequired, + toggleFilter: React.PropTypes.func.isRequired + }; + + renderValue () { + return ( + <div className="projects-filter-value"> + <Level level={this.props.value}/> + </div> + ); + } + + renderOptions () { + const options = ['ERROR', 'WARN', 'OK']; + + return ( + <div> + {options.map(option => ( + <Link key={option} + className={option === this.props.value ? 'active' : ''} + to={this.props.getFilterUrl({ gate: option })} + onClick={this.props.toggleFilter}> + <Level level={option}/> + </Link> + ))} + {this.props.value != null && ( + <div> + <hr/> + <Link className="text-center" to={this.props.getFilterUrl({ gate: null })} + onClick={this.props.toggleFilter}> + <span className="text-danger">{translate('reset_verb')}</span> + </Link> + </div> + )} + </div> + ); + } + + render () { + return ( + <Filter + renderName={() => 'Quality Gate'} + renderOptions={() => this.renderOptions()} + renderValue={() => this.renderValue()} + {...this.props}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js new file mode 100644 index 00000000000..13bc5b67b01 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js @@ -0,0 +1,25 @@ +/* + * 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 QualityGateFilter from './QualityGateFilter'; +import connectFilter from './connectFilter'; + +const getValue = query => query.gate; + +export default connectFilter('gate', getValue)(QualityGateFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js new file mode 100644 index 00000000000..3bd702dd3e0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js @@ -0,0 +1,127 @@ +/* + * 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 React from 'react'; +import { Link } from 'react-router'; +import Filter from './Filter'; +import SizeRating from '../../../components/ui/SizeRating'; +import { formatMeasure } from '../../../helpers/measures'; +import { translate } from '../../../helpers/l10n'; + +export default class SizeFilter extends React.Component { + static propTypes = { + value: React.PropTypes.shape({ + from: React.PropTypes.number, + to: React.PropTypes.number + }), + getFilterUrl: React.PropTypes.func.isRequired, + toggleFilter: React.PropTypes.func.isRequired + }; + + isOptionAction (from, to) { + const { value } = this.props; + + if (value == null) { + return false; + } + + return value.from === from && value.to === to; + } + + renderLabel (value) { + let label; + if (value.to == null) { + label = '>' + formatMeasure(value.from, 'SHORT_INT'); + } else if (value.from == null) { + label = '<' + formatMeasure(value.to, 'SHORT_INT'); + } else { + label = formatMeasure(value.from, 'SHORT_INT') + '–' + formatMeasure(value.to, 'SHORT_INT'); + } + return label; + } + + renderValue () { + const { value } = this.props; + + let average; + if (value.to == null) { + average = value.from; + } else if (value.from == null) { + average = value.to / 2; + } else { + average = (value.from + value.to) / 2; + } + + const label = this.renderLabel(value); + + return ( + <div className="projects-filter-value"> + <SizeRating value={average}/> + + <div className="projects-filter-hint note"> + {label} + </div> + </div> + ); + } + + renderOptions () { + const options = [ + [null, 1000, 0], + [1000, 10000, 1000], + [10000, 100000, 10000], + [100000, 500000, 100000], + [500000, null, 500000], + ]; + + return ( + <div> + {options.map(option => ( + <Link key={option[2]} + className={this.isOptionAction(option[0], option[1]) ? 'active' : ''} + to={this.props.getFilterUrl({ 'size__gte': option[0], 'size__lt': option[1] })} + onClick={this.props.toggleFilter}> + <SizeRating value={option[2]}/> + <span className="spacer-left">{this.renderLabel({ from: option[0], to: option[1] })}</span> + </Link> + ))} + {this.props.value != null && ( + <div> + <hr/> + <Link className="text-center" + to={this.props.getFilterUrl({ 'size__gte': null, 'size__lt': null })} + onClick={this.props.toggleFilter}> + <span className="text-danger">{translate('reset_verb')}</span> + </Link> + </div> + )} + </div> + ); + } + + render () { + return ( + <Filter + renderName={() => 'Size'} + renderOptions={() => this.renderOptions()} + renderValue={() => this.renderValue()} + {...this.props}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js new file mode 100644 index 00000000000..3cf6fd50e44 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js @@ -0,0 +1,29 @@ +/* + * 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 SizeFilter from './SizeFilter'; +import connectFilter from './connectFilter'; + +const getValue = query => { + const from = query['size__gte']; + const to = query['size__lt']; + return from == null && to == null ? null : { from, to }; +}; + +export default connectFilter('size', getValue)(SizeFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js new file mode 100644 index 00000000000..fed9b5a7980 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js @@ -0,0 +1,44 @@ +/* + * 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 { connect } from 'react-redux'; +import omitBy from 'lodash/omitBy'; +import isNil from 'lodash/isNil'; +import { getProjectsAppFilterStatus } from '../../../app/store/rootReducer'; +import { toggleFilter } from '../store/filters/statuses/actions'; +import { OPEN } from '../store/filters/statuses/reducer'; + +const connectFilter = (key, getValue) => Component => { + const mapStateToProps = (state, ownProps) => ({ + isOpen: getProjectsAppFilterStatus(state, key) === OPEN, + value: getValue(ownProps.query), + getFilterUrl: part => { + const query = omitBy({ ...ownProps.query, ...part }, isNil); + return { pathname: '/projects', query }; + } + }); + + const mapDispatchToProps = dispatch => ({ + toggleFilter: () => dispatch(toggleFilter(key)) + }); + + return connect(mapStateToProps, mapDispatchToProps)(Component); +}; + +export default connectFilter; diff --git a/server/sonar-web/src/main/js/apps/projects/routes.js b/server/sonar-web/src/main/js/apps/projects/routes.js new file mode 100644 index 00000000000..b96e7d3651b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/routes.js @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router'; +import AppContainer from './components/AppContainer'; + +export default ( + <Route path="projects" component={AppContainer}/> +); 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 new file mode 100644 index 00000000000..a97261c73d2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js @@ -0,0 +1,119 @@ +/* + * 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 groupBy from 'lodash/groupBy'; +import keyBy from 'lodash/keyBy'; +import { searchProjects } from '../../../api/components'; +import { addGlobalErrorMessage } from '../../../components/store/globalMessages'; +import { parseError } from '../../code/utils'; +import { receiveComponents } from '../../../app/store/components/actions'; +import { receiveProjects, receiveMoreProjects } from './projects/actions'; +import { updateState } from './state/actions'; +import { getProjectsAppState } from '../../../app/store/rootReducer'; +import { getMeasuresForComponents } from '../../../api/measures'; +import { receiveComponentsMeasures } from '../../../app/store/measures/actions'; +import { convertToFilter } from './utils'; + +const PAGE_SIZE = 50; + +const METRICS = [ + 'alert_status', + 'reliability_rating', + 'security_rating', + 'sqale_rating', + 'duplicated_lines_density', + 'coverage', + 'ncloc', + 'ncloc_language_distribution' +]; + +const onFail = dispatch => error => { + parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); + dispatch(updateState({ loading: false })); +}; + +const onReceiveMeasures = (dispatch, projects) => response => { + const projectsById = keyBy(projects, 'id'); + const byComponentId = groupBy(response.measures, 'component'); + + const toStore = {}; + + Object.keys(byComponentId).forEach(componentId => { + const componentKey = projectsById[componentId].key; + const measures = {}; + byComponentId[componentId].forEach(measure => { + measures[measure.metric] = measure.value; + }); + toStore[componentKey] = measures; + }); + + dispatch(receiveComponentsMeasures(toStore)); +}; + +const fetchProjectMeasures = projects => dispatch => { + if (!projects.length) { + return Promise.resolve(); + } + + const projectKeys = projects.map(project => project.key); + return getMeasuresForComponents(projectKeys, METRICS).then(onReceiveMeasures(dispatch, projects), onFail(dispatch)); +}; + +const onReceiveProjects = dispatch => response => { + dispatch(receiveComponents(response.components)); + dispatch(receiveProjects(response.components)); + dispatch(fetchProjectMeasures(response.components)).then(() => { + dispatch(updateState({ loading: false })); + }); + dispatch(updateState({ + total: response.paging.total, + pageIndex: response.paging.pageIndex, + })); +}; + +const onReceiveMoreProjects = dispatch => response => { + dispatch(receiveComponents(response.components)); + dispatch(receiveMoreProjects(response.components)); + dispatch(fetchProjectMeasures(response.components)).then(() => { + dispatch(updateState({ loading: false })); + }); + dispatch(updateState({ pageIndex: response.paging.pageIndex })); +}; + +export const fetchProjects = query => dispatch => { + dispatch(updateState({ loading: true })); + const data = { ps: PAGE_SIZE }; + const filter = convertToFilter(query); + if (filter) { + data.filter = filter; + } + return searchProjects(data).then(onReceiveProjects(dispatch), onFail(dispatch)); +}; + +export const fetchMoreProjects = query => (dispatch, getState) => { + dispatch(updateState({ loading: true })); + const state = getState(); + const { pageIndex } = getProjectsAppState(state); + const data = { ps: PAGE_SIZE, p: pageIndex + 1 }; + const filter = convertToFilter(query); + if (filter) { + data.filter = filter; + } + return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch)); +}; diff --git a/server/sonar-web/src/main/js/apps/projects/store/filters/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/filters/reducer.js new file mode 100644 index 00000000000..830f667057c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/filters/reducer.js @@ -0,0 +1,29 @@ +/* + * 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 { combineReducers } from 'redux'; +import statuses, * as fromStatuses from './statuses/reducer'; + +const reducer = combineReducers({ statuses }); + +export default reducer; + +export const getFilterStatus = (state, key) => ( + fromStatuses.getStatus(state.statuses, key) +); diff --git a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js new file mode 100644 index 00000000000..d21b9bd4505 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +export const TOGGLE_FILTER = 'projects/TOGGLE_FILTER'; + +export const toggleFilter = key => ({ + type: TOGGLE_FILTER, + key +}); diff --git a/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js new file mode 100644 index 00000000000..baf76f152a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js @@ -0,0 +1,44 @@ +/* + * 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 { TOGGLE_FILTER } from './actions'; + +export const OPEN = 'OPEN'; +export const CLOSED = 'CLOSED'; + +const allClosedState = { + coverage: CLOSED, + duplications: CLOSED, + size: CLOSED +}; + +const reducer = (state = allClosedState, action = {}) => { + if (action.type === TOGGLE_FILTER) { + const newStatus = state[action.key] === OPEN ? CLOSED : OPEN; + return { ...allClosedState, [action.key]: newStatus }; + } + + return state; +}; + +export default reducer; + +export const getStatus = (state, key) => ( + state[key] +); diff --git a/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js b/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js new file mode 100644 index 00000000000..0fe328cd60e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/projects/actions.js @@ -0,0 +1,32 @@ +/* + * 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. + */ +export const RECEIVE_PROJECTS = 'projects/RECEIVE_PROJECTS'; + +export const receiveProjects = projects => ({ + type: RECEIVE_PROJECTS, + projects +}); + +export const RECEIVE_MORE_PROJECTS = 'projects/RECEIVE_MORE_PROJECTS'; + +export const receiveMoreProjects = projects => ({ + type: RECEIVE_MORE_PROJECTS, + projects +}); diff --git a/server/sonar-web/src/main/js/apps/projects/store/projects/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/projects/reducer.js new file mode 100644 index 00000000000..d5669ada713 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/projects/reducer.js @@ -0,0 +1,37 @@ +/* + * 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 { RECEIVE_PROJECTS, RECEIVE_MORE_PROJECTS } from './actions'; + +const reducer = (state = null, action = {}) => { + if (action.type === RECEIVE_PROJECTS) { + return action.projects.map(project => project.key); + } + + if (action.type === 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 new file mode 100644 index 00000000000..4837b84ee04 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/reducer.js @@ -0,0 +1,37 @@ +/* + * 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 { combineReducers } from 'redux'; +import projects, * as fromProjects from './projects/reducer'; +import state from './state/reducer'; +import filters, * as fromFilters from './filters/reducer'; + +export default combineReducers({ projects, state, filters }); + +export const getProjects = state => ( + fromProjects.getProjects(state.projects) +); + +export const getState = state => ( + state.state +); + +export const getFilterStatus = (state, key) => ( + fromFilters.getFilterStatus(state.filters, key) +); diff --git a/server/sonar-web/src/main/js/apps/projects/store/state/actions.js b/server/sonar-web/src/main/js/apps/projects/store/state/actions.js new file mode 100644 index 00000000000..7046e08aa62 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/state/actions.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +export const UPDATE_STATE = 'projects/UPDATE_STATE'; + +export const updateState = changes => ({ + type: UPDATE_STATE, + changes +}); diff --git a/server/sonar-web/src/main/js/apps/projects/store/state/reducer.js b/server/sonar-web/src/main/js/apps/projects/store/state/reducer.js new file mode 100644 index 00000000000..853accdd5ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/state/reducer.js @@ -0,0 +1,30 @@ +/* + * 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 { UPDATE_STATE } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === UPDATE_STATE) { + return { ...state, ...action.changes }; + } + + return state; +}; + +export default reducer; 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 new file mode 100644 index 00000000000..110dc3fa5d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js @@ -0,0 +1,79 @@ +/* + * 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. + */ +const getAsNumber = value => { + if (value === '' || value == null) { + return null; + } + return isNaN(value) ? null : Number(value); +}; + +const getAsLevel = value => { + if (value === 'ERROR' || value === 'WARN' || value === 'OK') { + return value; + } + return null; +}; + +export const parseUrlQuery = urlQuery => ({ + 'gate': getAsLevel(urlQuery['gate']), + + 'coverage__gte': getAsNumber(urlQuery['coverage__gte']), + 'coverage__lt': getAsNumber(urlQuery['coverage__lt']), + + 'duplications__gte': getAsNumber(urlQuery['duplications__gte']), + 'duplications__lt': getAsNumber(urlQuery['duplications__lt']), + + 'size__gte': getAsNumber(urlQuery['size__gte']), + 'size__lt': getAsNumber(urlQuery['size__lt']) +}); + +export const convertToFilter = query => { + const conditions = []; + + if (query['gate'] != null) { + conditions.push('alert_status = ' + query['gate']); + } + + if (query['coverage__gte'] != null) { + conditions.push('coverage >= ' + query['coverage__gte']); + } + + if (query['coverage__lt'] != null) { + conditions.push('coverage < ' + query['coverage__lt']); + } + + if (query['duplications__gte'] != null) { + conditions.push('duplicated_lines_density >= ' + query['duplications__gte']); + } + + if (query['duplications__lt'] != null) { + conditions.push('duplicated_lines_density < ' + query['duplications__lt']); + } + + if (query['size__gte'] != null) { + conditions.push('ncloc >= ' + query['size__gte']); + } + + if (query['size__lt'] != null) { + conditions.push('ncloc < ' + query['size__lt']); + } + + return conditions.join(' and '); +}; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css new file mode 100644 index 00000000000..8c5913e0d4d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -0,0 +1,147 @@ +.projects-list { + outline: none; +} + +.projects-empty-list { + padding: 60px 0; + border: 1px solid #e6e6e6; + border-radius: 2px; + text-align: center; + color: #777; +} + +.project-card { + height: 115px; + box-sizing: border-box; +} + +.project-card-name { + font-weight: 600; +} + +.project-card-measures { + margin: 0 -20px; +} + +.project-card-measure { + position: relative; + display: inline-block; + vertical-align: top; + padding: 0 15px; + text-align: center; +} + +.project-card-measure + .project-card-measure:before { + position: absolute; + top: 50%; + left: 0; + width: 0; + height: 24px; + margin-top: -12px; + border-left: 1px solid #e6e6e6; + content: ""; +} + +.project-card-measure.pull-right:before { + display: none; +} + +.project-card-measure .level { + margin-top: 0; + margin-bottom: 0; +} + +.project-card-measure-inner { + display: inline-block; + vertical-align: top; + text-align: center; +} + +.project-card-measure-number { + line-height: 24px; + font-size: 18px; +} + +.project-card-measure-label { + margin-top: 4px; + font-size: 12px; +} + +.projects-filter { + border: 1px solid transparent; + border-top-color: #e6e6e6; +} + +.projects-filter:first-child { + border-top-color: transparent; +} + +.projects-filter-active .projects-filter-name { + font-weight: 600; +} + +.projects-filter-open { + border-color: #e6e6e6 !important; + background-color: #fff; +} + +.projects-filter-open + .projects-filter { + border-top-color: transparent; +} + +.projects-filter-open .projects-filter-header { + border-bottom: 1px solid #e6e6e6; + transition: none; +} + +.projects-filter-header { + display: block; + padding: 15px; + color: #444; + border: none; +} + +.projects-filter-checkbox { + float: left; + padding: 4px 4px 4px 0; +} + +.projects-filter-name { + float: left; + padding: 4px; + line-height: 16px; +} + +.projects-filter-hint { + float: left; + margin-right: 6px; + padding: 4px; + line-height: 16px; +} + +.projects-filter-value { + float: right; +} + +.projects-filter-options { + padding-top: 5px; + padding-bottom: 5px; +} + +.projects-filter-options a { + display: block; + padding: 5px 15px; + line-height: 24px; + border: none; + color: #444; +} + +.projects-filter-options a:hover, +.projects-filter-options a:active, +.projects-filter-options a:focus { + background-color: #f3f3f3; +} + +.projects-filter-options a.active { + background-color: #cae3f2; +} diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js index 2e2123a43a9..b6a2c289cfd 100644 --- a/server/sonar-web/src/main/js/components/shared/drilldown-link.js +++ b/server/sonar-web/src/main/js/components/shared/drilldown-link.js @@ -119,7 +119,7 @@ export const DrilldownLink = React.createClass({ this.propsToIssueParams()); return ( - <a href={url}>{this.props.children}</a> + <a className={this.props.className} href={url}>{this.props.children}</a> ); }, diff --git a/server/sonar-web/src/main/js/components/ui/CoverageRating.js b/server/sonar-web/src/main/js/components/ui/CoverageRating.js new file mode 100644 index 00000000000..d6d6d54d85b --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/CoverageRating.js @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import { DonutChart } from '../charts/donut-chart'; + +export default class CoverageRating extends React.Component { + static propTypes = { + value: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) + }; + + render () { + let data = [{ value: 100, fill: '#ccc ' }]; + + if (this.props.value != null) { + const value = Number(this.props.value); + data = [ + { value, fill: '#85bb43' }, + { value: 100 - value, fill: '#d4333f' } + ]; + } + + return ( + <DonutChart + data={data} + width={24} + height={24} + thickness={3}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/ui/DuplicationsRating.css b/server/sonar-web/src/main/js/components/ui/DuplicationsRating.css new file mode 100644 index 00000000000..73c12db92df --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/DuplicationsRating.css @@ -0,0 +1,56 @@ +.duplications-rating { + position: relative; + display: inline-flex; + vertical-align: top; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + border: 3px solid #f3ca8e; + border-radius: 24px; + box-sizing: border-box; +} + +.duplications-rating:after { + border-radius: 24px; + background-color: #f3ca8e; + content: ""; +} + +.duplications-rating-A { + border-color: #85bb43; +} + +.duplications-rating-A:after { + display: none; +} + +.duplications-rating-B:after { + width: 6px; + height: 6px; +} + +.duplications-rating-C:after { + width: 8px; + height: 8px; +} + +.duplications-rating-D { + border-color: #d4333f; +} + +.duplications-rating-D:after { + width: 12px; + height: 12px; + background-color: #d4333f; +} + +.duplications-rating-E { + border-color: #d4333f; +} + +.duplications-rating-E:after { + width: 14px; + height: 14px; + background-color: #d4333f; +} diff --git a/server/sonar-web/src/main/js/components/ui/DuplicationsRating.js b/server/sonar-web/src/main/js/components/ui/DuplicationsRating.js new file mode 100644 index 00000000000..35765a68a37 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/DuplicationsRating.js @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import classNames from 'classnames'; +import inRange from 'lodash/inRange'; +import './DuplicationsRating.css'; + +export default class DuplicationsRating extends React.Component { + static propTypes = { + value: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]).isRequired + }; + + render () { + const { value } = this.props; + const className = classNames('duplications-rating', { + 'duplications-rating-A': inRange(value, 3), + 'duplications-rating-B': inRange(value, 3, 5), + 'duplications-rating-C': inRange(value, 5, 10), + 'duplications-rating-D': inRange(value, 10, 20), + 'duplications-rating-E': value >= 20 + }); + + return ( + <div className={className}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/ui/Level.css b/server/sonar-web/src/main/js/components/ui/Level.css new file mode 100644 index 00000000000..77075bcb679 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/Level.css @@ -0,0 +1,45 @@ +.level { + display: inline-block; + width: 80px; + height: 24px; + line-height: 24px; + border-radius: 24px; + box-sizing: border-box; + color: #fff; + letter-spacing: 0.02em; + font-size: 13px; + font-weight: 400; + text-align: center; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.35); +} + +.level-small { + width: 64px; + margin-top: -1px; + margin-bottom: -1px; + height: 18px; + line-height: 18px; + font-size: 12px; +} + +a > .level { + margin-bottom: -1px; + border-bottom: 1px solid; + transition: all 0.2s ease; +} + +a > .level:hover { + opacity: 0.8; +} + +.level-OK { + background-color: #85bb43; +} + +.level-WARN { + background-color: #f90; +} + +.level-ERROR { + background-color: #d4333f; +} diff --git a/server/sonar-web/src/main/js/components/ui/Level.js b/server/sonar-web/src/main/js/components/ui/Level.js index 7da8cf05d7b..69f1c512822 100644 --- a/server/sonar-web/src/main/js/components/ui/Level.js +++ b/server/sonar-web/src/main/js/components/ui/Level.js @@ -19,6 +19,7 @@ */ import React from 'react'; import { formatMeasure } from '../../helpers/measures'; +import './Level.css'; export default class Level extends React.Component { static propTypes = { diff --git a/server/sonar-web/src/main/js/components/ui/Rating.css b/server/sonar-web/src/main/js/components/ui/Rating.css new file mode 100644 index 00000000000..31d8bb6f4d3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/Rating.css @@ -0,0 +1,57 @@ +.rating { + display: inline-block; + width: 24px; + height: 24px; + line-height: 23px; + border-radius: 24px; + box-sizing: border-box; + color: #fff; + font-size: 16px; + font-weight: 400; + text-align: center; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.35); +} + +a > .rating { + margin-bottom: -1px; + border-bottom: 1px solid; + transition: all 0.2s ease; +} + +a > .rating:hover { + opacity: 0.8; +} + +.rating-A { + background-color: #0a0; +} + +a > .rating-A { + border-bottom-color: #0a0; +} + +.rating-B { + background-color: #80cc00; +} + +a .rating-B { border-bottom-color: #80cc00; } + +.rating-C { + background-color: #fe0; + color: #444; + text-shadow: none; +} + +a .rating-C { border-bottom-color: #fe0; } + +.rating-D { + background-color: #f77700; +} + +a .rating-D { border-bottom-color: #f77700; } + +.rating-E { + background-color: #e00; +} + +a .rating-E { border-bottom-color: #e00; } diff --git a/server/sonar-web/src/main/js/components/ui/Rating.js b/server/sonar-web/src/main/js/components/ui/Rating.js index 90b0c75cc5e..74e61d2b624 100644 --- a/server/sonar-web/src/main/js/components/ui/Rating.js +++ b/server/sonar-web/src/main/js/components/ui/Rating.js @@ -19,6 +19,7 @@ */ import React from 'react'; import { formatMeasure } from '../../helpers/measures'; +import './Rating.css'; export default class Rating extends React.Component { static propTypes = { diff --git a/server/sonar-web/src/main/js/components/ui/SizeRating.css b/server/sonar-web/src/main/js/components/ui/SizeRating.css new file mode 100644 index 00000000000..7aeb21a922a --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/SizeRating.css @@ -0,0 +1,16 @@ +.size-rating { + display: inline-block; + vertical-align: top; + width: 24px; + height: 24px; + line-height: 24px; + border-radius: 24px; + background-color: #4b9fd5; + color: #fff; + font-size: 12px; + text-align: center; +} + +.size-rating-muted { + background-color: #ccc; +} diff --git a/server/sonar-web/src/main/js/components/ui/SizeRating.js b/server/sonar-web/src/main/js/components/ui/SizeRating.js new file mode 100644 index 00000000000..40ed0ebd8c5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/SizeRating.js @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import inRange from 'lodash/inRange'; +import './SizeRating.css'; + +export default class SizeRating extends React.Component { + static propTypes = { + value: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string]) + }; + + render () { + const { value } = this.props; + + if (value == null) { + return ( + <div className="size-rating size-rating-muted"> </div> + ); + } + + let letter; + if (inRange(value, 1000)) { + letter = 'XS'; + } else if (inRange(value, 1000, 10000)) { + letter = 'S'; + } else if (inRange(value, 10000, 100000)) { + letter = 'M'; + } else if (inRange(value, 100000, 500000)) { + letter = 'L'; + } else if (value >= 500000) { + letter = 'XL'; + } + + return ( + <div className="size-rating">{letter}</div> + ); + } +} diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 35307e33a54..894d9e1e257 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -115,5 +115,5 @@ export function getDeprecatedActiveRulesUrl (query = {}) { } export const getProjectsUrl = () => { - return window.baseUrl + '/measures/search?qualifiers[]=TRK'; + return window.baseUrl + '/projects'; }; diff --git a/server/sonar-web/src/main/js/main/app.js b/server/sonar-web/src/main/js/main/app.js index fae8f401816..6e41663d2b8 100644 --- a/server/sonar-web/src/main/js/main/app.js +++ b/server/sonar-web/src/main/js/main/app.js @@ -28,6 +28,7 @@ import Navigation from './nav/app'; import { installGlobal, requestMessages } from '../helpers/l10n'; import * as measures from '../helpers/measures'; import * as request from '../helpers/request'; +import './common-styles'; import '../../less/sonar.less'; diff --git a/server/sonar-web/src/main/js/main/common-styles.js b/server/sonar-web/src/main/js/main/common-styles.js new file mode 100644 index 00000000000..cf26f70de29 --- /dev/null +++ b/server/sonar-web/src/main/js/main/common-styles.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// this is a bad idea +// TODO rework once we have single js app + +import '../components/ui/Level.css'; +import '../components/ui/Rating.css'; diff --git a/server/sonar-web/src/main/js/main/nav/global/global-nav-menu.js b/server/sonar-web/src/main/js/main/nav/global/global-nav-menu.js index 36938d47123..3f76148af2e 100644 --- a/server/sonar-web/src/main/js/main/nav/global/global-nav-menu.js +++ b/server/sonar-web/src/main/js/main/nav/global/global-nav-menu.js @@ -71,6 +71,15 @@ export default React.createClass({ ); }, + renderProjects () { + const url = window.baseUrl + '/projects'; + return ( + <li className={this.activeLink('/projects')}> + <a href={url}>{translate('projects.page')}</a> + </li> + ); + }, + renderIssuesLink () { const url = window.baseUrl + '/issues/search'; return ( @@ -159,6 +168,7 @@ export default React.createClass({ return ( <ul className="nav navbar-nav"> {this.renderDashboards()} + {this.renderProjects()} {this.renderIssuesLink()} {this.renderMeasuresLink()} {this.renderRulesLink()} diff --git a/server/sonar-web/src/main/js/main/nav/global/search-view.js b/server/sonar-web/src/main/js/main/nav/global/search-view.js index e061eb90a9a..822d55a524b 100644 --- a/server/sonar-web/src/main/js/main/nav/global/search-view.js +++ b/server/sonar-web/src/main/js/main/nav/global/search-view.js @@ -179,13 +179,15 @@ export default Marionette.LayoutView.extend({ const favorite = _.first(this.favorite, 6).map(function (f, index) { return _.extend(f, { extra: index === 0 ? translate('favorite') : null }); }); - const qualifiers = this.model.get('qualifiers').map(function (q, index) { - return { - url: window.baseUrl + '/all_projects?qualifier=' + encodeURIComponent(q), - name: translate('qualifiers.all', q), - extra: index === 0 ? '' : null - }; - }); + const qualifiers = this.model.get('qualifiers') + .filter(q => q !== 'TRK') + .map(function (q, index) { + return { + url: window.baseUrl + '/all_projects?qualifier=' + encodeURIComponent(q), + name: translate('qualifiers.all', q), + extra: index === 0 ? '' : null + }; + }); this.results.reset([].concat(history, favorite, qualifiers)); }, @@ -211,7 +213,7 @@ export default Marionette.LayoutView.extend({ q: domain.q, extra: index === 0 ? domain.name : null, url: window.baseUrl + '/dashboard/index?id=' + encodeURIComponent(item.key) + - window.dashboardParameters(true) + window.dashboardParameters(true) })); }); }); diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less index 8d4efa32d67..5515bc26485 100644 --- a/server/sonar-web/src/main/less/components/ui.less +++ b/server/sonar-web/src/main/less/components/ui.less @@ -20,90 +20,6 @@ @import (reference) "../variables"; @import (reference) "../mixins"; - -/* - * Display Maintainability Rating (also used by the SQALE plugin) - */ - -.rating { - display: inline-block; - width: 1em; - height: 1.3em; - line-height: 1.3; - color: #fff; - font-weight: 300; - text-align: center; - - a > & { - margin-bottom: -1px; - border-bottom: 1px solid; - transition: all 0.2s ease; - - &:hover { opacity: 0.8; } - } -} - -.rating-A { - background-color: #00AA00; - a & { border-bottom-color: #00AA00; } -} - -.rating-B { - background-color: #80CC00; - a & { border-bottom-color: #80CC00; } -} - -.rating-C { - background-color: #FFEE00; - color: @baseFontColor; - a & { border-bottom-color: #FFEE00; } -} - -.rating-D { - background-color: #F77700; - a & { border-bottom-color: #F77700; } -} - -.rating-E { - background-color: #EE0000; - a & { border-bottom-color: #EE0000; } -} - -.level { - display: inline-block; - height: 1.6em; - line-height: 1.7; - margin: -0.3em 0; - padding: 0 0.5em; - border-radius: 3px; - color: #fff; - letter-spacing: 0.02em; - font-weight: 400; - text-align: center; - text-shadow: 0 0 1px rgba(0, 0, 0, 0.35); - - a > & { - margin-bottom: -1px; - border-bottom: 1px solid; - transition: all 0.2s ease; - - &:hover { opacity: 0.8; } - } -} - -.level-OK { - background-color: @green; -} - -.level-WARN { - background-color: @orange; -} - -.level-ERROR { - background-color: @red; -} - - .processes-container { position: fixed; z-index: @process-container-z-index; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb new file mode 100644 index 00000000000..1174555457f --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb @@ -0,0 +1,26 @@ +# +# SonarQube, open source software quality management tool. +# Copyright (C) 2008-2014 SonarSource +# mailto:contact AT sonarsource DOT com +# +# SonarQube 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. +# +# SonarQube 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. +# +class ProjectsController < ApplicationController + + def index + + end + +end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb new file mode 100644 index 00000000000..513b41c5692 --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb @@ -0,0 +1,3 @@ +<% content_for :extra_script do %> + <script src="<%= ApplicationController.root_context -%>/js/bundles/app.js?v=<%= sonar_version -%>"></script> +<% end %> |