aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/projects
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-10-18 09:46:22 +0200
committerStas Vilchik <vilchiks@gmail.com>2016-10-21 10:24:17 +0200
commitb7129679327efeeb44f9205656b46376adfe9689 (patch)
treef01159db52a79aae8e478dcafe79a7d9d22156c6 /server/sonar-web/src/main/js/apps/projects
parent3d8cdcbf8558e40385f481272045056d5435a3e6 (diff)
downloadsonarqube-b7129679327efeeb44f9205656b46376adfe9689.tar.gz
sonarqube-b7129679327efeeb44f9205656b46376adfe9689.zip
SONAR-8300 Create new "Projects" page [first iter]
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projects')
-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
36 files changed, 2024 insertions, 0 deletions
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;
+}