diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-10-18 09:46:22 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-10-21 10:24:17 +0200 |
commit | b7129679327efeeb44f9205656b46376adfe9689 (patch) | |
tree | f01159db52a79aae8e478dcafe79a7d9d22156c6 /server/sonar-web/src/main/js/apps/projects | |
parent | 3d8cdcbf8558e40385f481272045056d5435a3e6 (diff) | |
download | sonarqube-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')
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; +} |