aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/package.json1
-rw-r--r--server/sonar-web/src/main/js/api/components.js5
-rw-r--r--server/sonar-web/src/main/js/api/measures.js7
-rw-r--r--server/sonar-web/src/main/js/app/components/App.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js28
-rw-r--r--server/sonar-web/src/main/js/app/index.js4
-rw-r--r--server/sonar-web/src/main/js/app/store/components/actions.js25
-rw-r--r--server/sonar-web/src/main/js/app/store/components/reducer.js47
-rw-r--r--server/sonar-web/src/main/js/app/store/languages/actions.js25
-rw-r--r--server/sonar-web/src/main/js/app/store/languages/reducer.js35
-rw-r--r--server/sonar-web/src/main/js/app/store/measures/actions.js15
-rw-r--r--server/sonar-web/src/main/js/app/store/measures/reducer.js25
-rw-r--r--server/sonar-web/src/main/js/app/store/rootActions.js34
-rw-r--r--server/sonar-web/src/main/js/app/store/rootReducer.js45
-rw-r--r--server/sonar-web/src/main/js/app/styles/boxed-group.css (renamed from server/sonar-web/src/main/js/app/styles.css)0
-rw-r--r--server/sonar-web/src/main/js/app/styles/index.js21
-rw-r--r--server/sonar-web/src/main/js/app/styles/page.css26
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Measure.js5
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/enhance.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/App.js82
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AppContainer.js27
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeader.js52
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js47
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardContainer.js29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.js55
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardMeasures.js129
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js49
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js75
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsListContainer.js28
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooter.js35
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsListFooterContainer.js42
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js126
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilterContainer.js29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js126
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilterContainer.js29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.js70
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js76
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilterContainer.js25
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js127
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilterContainer.js29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/connectFilter.js44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/routes.js26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/actions.js119
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/filters/reducer.js29
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/filters/statuses/actions.js25
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/filters/statuses/reducer.js44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/projects/actions.js32
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/projects/reducer.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/reducer.js37
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/state/actions.js25
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/state/reducer.js30
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/utils.js79
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css147
-rw-r--r--server/sonar-web/src/main/js/components/shared/drilldown-link.js2
-rw-r--r--server/sonar-web/src/main/js/components/ui/CoverageRating.js47
-rw-r--r--server/sonar-web/src/main/js/components/ui/DuplicationsRating.css56
-rw-r--r--server/sonar-web/src/main/js/components/ui/DuplicationsRating.js44
-rw-r--r--server/sonar-web/src/main/js/components/ui/Level.css45
-rw-r--r--server/sonar-web/src/main/js/components/ui/Level.js1
-rw-r--r--server/sonar-web/src/main/js/components/ui/Rating.css57
-rw-r--r--server/sonar-web/src/main/js/components/ui/Rating.js1
-rw-r--r--server/sonar-web/src/main/js/components/ui/SizeRating.css16
-rw-r--r--server/sonar-web/src/main/js/components/ui/SizeRating.js55
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.js2
-rw-r--r--server/sonar-web/src/main/js/main/app.js1
-rw-r--r--server/sonar-web/src/main/js/main/common-styles.js25
-rw-r--r--server/sonar-web/src/main/js/main/nav/global/global-nav-menu.js10
-rw-r--r--server/sonar-web/src/main/js/main/nav/global/search-view.js18
-rw-r--r--server/sonar-web/src/main/less/components/ui.less84
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb26
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb3
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">&nbsp;</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 %>