diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-05-29 12:18:58 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-06-09 08:26:48 +0200 |
commit | 8a173aa3175c846291b8b56f2cfd84ab0e782627 (patch) | |
tree | c3fafd7d782521c25ccd93dcd5816103f3f5145c /server/sonar-web/src/main/js/apps | |
parent | eb029461e77fbb1f7a9dde462512d88fc65d46b4 (diff) | |
download | sonarqube-8a173aa3175c846291b8b56f2cfd84ab0e782627.tar.gz sonarqube-8a173aa3175c846291b8b56f2cfd84ab0e782627.zip |
SONAR-9254 Move projects facets sorting to the topbar of the projects page
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
21 files changed, 729 insertions, 425 deletions
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index 1c64c117f1f..8b172222b75 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -21,13 +21,14 @@ import React from 'react'; import Helmet from 'react-helmet'; import PageHeaderContainer from './PageHeaderContainer'; -import ProjectOptionBar from './ProjectOptionBar'; +import ProjectsOptionBar from './ProjectsOptionBar'; import ProjectsListContainer from './ProjectsListContainer'; import ProjectsListFooterContainer from './ProjectsListFooterContainer'; import PageSidebar from './PageSidebar'; import VisualizationsContainer from '../visualizations/VisualizationsContainer'; import { parseUrlQuery } from '../store/utils'; import { translate } from '../../../helpers/l10n'; +import { SORTING_SWITCH, parseSorting } from '../utils'; import '../styles.css'; type Props = { @@ -76,14 +77,30 @@ export default class AllProjects extends React.PureComponent { handleOptionBarToggle = (open: boolean) => this.setState({ optionBarOpen: open }); handlePerspectiveChange = ({ view, visualization }: { view: string, visualization?: string }) => { - this.props.router.push({ - pathname: this.props.location.pathname, - query: { - ...this.props.location.query, - view: view === 'overall' ? undefined : view, - visualization + const query: { view: ?string, visualization: ?string, sort?: ?string } = { + view: view === 'overall' ? undefined : view, + visualization + }; + + if (this.state.query.view === 'leak' || view === 'leak') { + if (this.state.query.sort) { + const sort = parseSorting(this.state.query.sort); + if (SORTING_SWITCH[sort.sortValue]) { + query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue]; + } } - }); + this.props.router.push({ pathname: this.props.location.pathname, query }); + } else { + this.updateLocationQuery(query); + } + }; + + handleSortChange = (sort: string, desc: boolean) => { + if (sort === 'name' && !desc) { + this.updateLocationQuery({ sort: undefined }); + } else { + this.updateLocationQuery({ sort: (desc ? '-' : '') + sort }); + } }; handleQueryChange() { @@ -92,12 +109,23 @@ export default class AllProjects extends React.PureComponent { this.props.fetchProjects(query, this.props.isFavorite, this.props.organization); } + updateLocationQuery = (newQuery: { [string]: ?string }) => { + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...this.props.location.query, + ...newQuery + } + }); + }; + render() { const { query, optionBarOpen } = this.state; const isFiltered = Object.keys(query).some(key => query[key] != null); const view = query.view || 'overall'; const visualization = query.visualization || 'risk'; + const selectedSort = query.sort || 'name'; const top = (this.props.organization ? 95 : 30) + (optionBarOpen ? 45 : 0); @@ -105,10 +133,12 @@ export default class AllProjects extends React.PureComponent { <div> <Helmet title={translate('projects.page')} /> - <ProjectOptionBar + <ProjectsOptionBar onPerspectiveChange={this.handlePerspectiveChange} + onSortChange={this.handleSortChange} onToggleOptionBar={this.handleOptionBarToggle} open={optionBarOpen} + selectedSort={selectedSort} view={view} visualization={visualization} /> 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 index 3b65bfd5d8d..f3866ee3935 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js @@ -30,9 +30,9 @@ type Props = { export default function PageHeader(props: Props) { return ( <header className="page-header"> - <div className="page-actions projects-page-actions"> - <div className="text-right spacer-bottom"> - <a className="button" href="#" onClick={props.onOpenOptionBar}> + <div className="page-actions projects-page-actions text-right"> + <div className="spacer-bottom"> + <a className="button js-projects-topbar-open" href="#" onClick={props.onOpenOptionBar}> {translate('projects.view_settings')} </a> </div> 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 index 85d9184784d..3e7ca9e4591 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js @@ -30,7 +30,7 @@ import NewDuplicationsFilter from '../filters/NewDuplicationsFilter'; import NewMaintainabilityFilter from '../filters/NewMaintainabilityFilter'; import NewReliabilityFilter from '../filters/NewReliabilityFilter'; import NewSecurityFilter from '../filters/NewSecurityFilter'; -import NewSizeFilter from '../filters/NewSizeFilter'; +import NewLinesFilter from '../filters/NewLinesFilter'; import QualityGateFilter from '../filters/QualityGateFilter'; import ReliabilityFilter from '../filters/ReliabilityFilter'; import SecurityFilter from '../filters/SecurityFilter'; @@ -101,7 +101,7 @@ export default function PageSidebar({ <NewMaintainabilityFilter key="new_maintainability" {...facetProps} />, <NewCoverageFilter key="new_coverage" {...facetProps} />, <NewDuplicationsFilter key="new_duplications" {...facetProps} />, - <NewSizeFilter key="new_size" {...facetProps} /> + <NewLinesFilter key="new_size" {...facetProps} /> ]} <LanguagesFilterContainer {...facetProps} /> <TagsFilterContainer {...facetProps} /> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js index ca31ad7fbe9..2580a34c4b8 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js @@ -27,6 +27,7 @@ import { VIEWS, VISUALIZATIONS } from '../utils'; export type Option = { label: string, type: string, value: string }; type Props = { + className?: string, onChange: ({ view: string, visualization?: string }) => void, view: string, visualization?: string @@ -64,7 +65,7 @@ export default class PerspectiveSelect extends React.PureComponent { const { view, visualization } = this.props; const perspective = view === 'visualizations' ? visualization : view; return ( - <div> + <div className={this.props.className}> <label>{translate('projects.perspective')}:</label> <Select className="little-spacer-left input-medium" diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBar.js index b506baf520d..e48872ff96e 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBar.js @@ -22,16 +22,19 @@ import React from 'react'; import classNames from 'classnames'; import CloseIcon from '../../../components/icons-components/CloseIcon'; import PerspectiveSelect from './PerspectiveSelect'; +import ProjectsSortingSelect from './ProjectsSortingSelect'; type Props = { onPerspectiveChange: ({ view: string, visualization?: string }) => void, + onSortChange: (sort: string, desc: boolean) => void, onToggleOptionBar: boolean => void, open: boolean, + selectedSort: string, view: string, visualization?: string }; -export default class ProjectOptionBar extends React.PureComponent { +export default class ProjectsOptionBar extends React.PureComponent { props: Props; closeBar = (evt: Event & { currentTarget: HTMLElement }) => { @@ -45,18 +48,22 @@ export default class ProjectOptionBar extends React.PureComponent { return ( <div className="projects-topbar"> <div className={classNames('projects-topbar-actions', { open })}> - <a - className="projects-topbar-button projects-topbar-button-close" - href="#" - onClick={this.closeBar}> + <a className="projects-topbar-button button-icon" href="#" onClick={this.closeBar}> <CloseIcon /> </a> <div className="projects-topbar-actions-inner"> <PerspectiveSelect + className="projects-topbar-item js-projects-perspective-select" onChange={this.props.onPerspectiveChange} view={this.props.view} visualization={this.props.visualization} /> + <ProjectsSortingSelect + className="projects-topbar-item js-projects-sorting-select" + onChange={this.props.onSortChange} + selectedSort={this.props.selectedSort} + view={this.props.view} + /> </div> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.js new file mode 100644 index 00000000000..4a5556bd91d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.js @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Select from 'react-select'; +import ProjectsSortingSelectOption from './ProjectsSortingSelectOption'; +import SortAscIcon from '../../../components/icons-components/SortAscIcon'; +import SortDescIcon from '../../../components/icons-components/SortDescIcon'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import { SORTING_METRICS, SORTING_LEAK_METRICS, parseSorting } from '../utils'; + +export type Option = { label: string, value: string, complement?: string, short?: string }; + +type Props = { + className?: string, + onChange: (sort: string, desc: boolean) => void, + selectedSort: string, + view: string +}; + +type State = { + sortValue: string, + sortDesc: boolean +}; + +export default class ProjectsSortingSelect extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = parseSorting(props.selectedSort); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.selectedSort !== this.props.selectedSort) { + this.setState(parseSorting(this.props.selectedSort)); + } + } + + getOptions = () => { + const sortMetrics = this.props.view === 'leak' ? SORTING_LEAK_METRICS : SORTING_METRICS; + return sortMetrics.map((opt: { value: string, complement?: string }) => ({ + value: opt.value, + label: translate('projects.sorting', opt.value), + complement: opt.complement && translate('projects.sorting', opt.complement), + short: opt.complement && translate('projects.sorting', opt.value, 'short') + })); + }; + + handleDescToggle = (evt: Event & { currentTarget: HTMLElement }) => { + evt.preventDefault(); + evt.currentTarget.blur(); + this.props.onChange(this.state.sortValue, !this.state.sortDesc); + }; + + handleSortChange = (option: Option) => this.props.onChange(option.value, this.state.sortDesc); + + render() { + const { sortDesc } = this.state; + + return ( + <div className={this.props.className}> + <label>{translate('projects.sort_by')}:</label> + <Select + className="little-spacer-left input-large" + clearable={false} + onChange={this.handleSortChange} + options={this.getOptions()} + optionComponent={ProjectsSortingSelectOption} + searchable={false} + value={this.state.sortValue} + /> + <Tooltip + overlay={ + sortDesc ? translate('projects.sort_descending') : translate('projects.sort_ascending') + }> + <a className="spacer-left button-icon" href="#" onClick={this.handleDescToggle}> + {sortDesc + ? <SortDescIcon className="little-spacer-top" /> + : <SortAscIcon className="little-spacer-top" />} + </a> + </Tooltip> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelectOption.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelectOption.js new file mode 100644 index 00000000000..f3613f1153e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelectOption.js @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +//@flow +import React from 'react'; +import type { Option } from './ProjectsSortingSelect'; + +type Props = { + option: Option, + children?: Element | Text, + className?: string, + isFocused?: boolean, + onFocus: (Option, MouseEvent) => void, + onSelect: (Option, MouseEvent) => void +}; + +export default class ProjectsSortingSelectOption extends React.PureComponent { + props: Props; + + handleMouseDown = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + this.props.onSelect(this.props.option, event); + }; + + handleMouseEnter = (event: MouseEvent) => { + this.props.onFocus(this.props.option, event); + }; + + handleMouseMove = (event: MouseEvent) => { + if (this.props.isFocused) { + return; + } + this.props.onFocus(this.props.option, event); + }; + + render() { + const { option } = this.props; + return ( + <div + className={this.props.className} + onMouseDown={this.handleMouseDown} + onMouseEnter={this.handleMouseEnter} + onMouseMove={this.handleMouseMove} + title={option.label}> + {option.short ? option.short : this.props.children} + {option.complement && + <div className="pull-right text-muted-2"> + {option.complement} + </div>} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js index b8667a28700..338395a6b84 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js @@ -19,22 +19,28 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import ProjectOptionBar from '../ProjectOptionBar'; +import ProjectsOptionBar from '../ProjectsOptionBar'; import { click } from '../../../../helpers/testUtils'; it('should render option bar closed', () => { - expect(shallow(<ProjectOptionBar open={false} view="overall" />)).toMatchSnapshot(); + expect(shallow(<ProjectsOptionBar open={false} view="overall" />)).toMatchSnapshot(); }); it('should render option bar open', () => { expect( - shallow(<ProjectOptionBar open={true} view="visualizations" visualization="coverage" />) + shallow(<ProjectsOptionBar open={true} view="leak" visualization="risk" />) + ).toMatchSnapshot(); +}); + +it.skip('should not render sorting options for visualizations', () => { + expect( + shallow(<ProjectsOptionBar open={true} view="visualizations" visualization="coverage" />) ).toMatchSnapshot(); }); it('should call close method correctly', () => { const toggle = jest.fn(); - const wrapper = shallow(<ProjectOptionBar open={true} view="leak" onToggleOptionBar={toggle} />); - click(wrapper.find('a.projects-topbar-button-close')); + const wrapper = shallow(<ProjectsOptionBar open={true} view="leak" onToggleOptionBar={toggle} />); + click(wrapper.find('a.projects-topbar-button')); expect(toggle.mock.calls).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.js new file mode 100644 index 00000000000..472558d0085 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import ProjectsSortingSelect from '../ProjectsSortingSelect'; + +it('should render correctly for overall view', () => { + expect(shallow(<ProjectsSortingSelect selectedSort="name" view="overall" />)).toMatchSnapshot(); +}); + +it('should render correctly for leak view', () => { + expect( + shallow(<ProjectsSortingSelect selectedSort="new_coverage" view="leak" />) + ).toMatchSnapshot(); +}); + +it('should handle the descending sort direction', () => { + expect( + shallow(<ProjectsSortingSelect selectedSort="-vulnerability" view="overall" />) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap index fdbeb7ed11b..5460d9a0e3d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap @@ -97,7 +97,7 @@ exports[`should render \`leak\` view correctly 1`] = ` } } /> - <NewSizeFilter + <NewLinesFilter isFavorite={false} property="new_lines" query={ diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap index ce43bf57d1f..27dff385bd9 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap @@ -8,6 +8,33 @@ Array [ ] `; +exports[`should not render sorting options for visualizations 1`] = ` +<div + className="projects-topbar" +> + <div + className="projects-topbar-actions open" + > + <a + className="projects-topbar-button button-icon" + href="#" + onClick={[Function]} + > + <CloseIcon /> + </a> + <div + className="projects-topbar-actions-inner" + > + <PerspectiveSelect + className="projects-topbar-item" + view="visualizations" + visualization="coverage" + /> + </div> + </div> +</div> +`; + exports[`should render option bar closed 1`] = ` <div className="projects-topbar" @@ -16,7 +43,7 @@ exports[`should render option bar closed 1`] = ` className="projects-topbar-actions" > <a - className="projects-topbar-button projects-topbar-button-close" + className="projects-topbar-button button-icon" href="#" onClick={[Function]} > @@ -26,6 +53,11 @@ exports[`should render option bar closed 1`] = ` className="projects-topbar-actions-inner" > <PerspectiveSelect + className="projects-topbar-item js-projects-perspective-select" + view="overall" + /> + <ProjectsSortingSelect + className="projects-topbar-item js-projects-sorting-select" view="overall" /> </div> @@ -41,7 +73,7 @@ exports[`should render option bar open 1`] = ` className="projects-topbar-actions open" > <a - className="projects-topbar-button projects-topbar-button-close" + className="projects-topbar-button button-icon" href="#" onClick={[Function]} > @@ -51,8 +83,13 @@ exports[`should render option bar open 1`] = ` className="projects-topbar-actions-inner" > <PerspectiveSelect - view="visualizations" - visualization="coverage" + className="projects-topbar-item js-projects-perspective-select" + view="leak" + visualization="risk" + /> + <ProjectsSortingSelect + className="projects-topbar-item js-projects-sorting-select" + view="leak" /> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.js.snap new file mode 100644 index 00000000000..c5ef95acbd7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.js.snap @@ -0,0 +1,352 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should handle the descending sort direction 1`] = ` +<div> + <label> + projects.sort_by + : + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + className="little-spacer-left input-large" + clearAllText="Clear all" + clearValueText="Clear value" + clearable={false} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + openAfterFocus={false} + optionComponent={[Function]} + options={ + Array [ + Object { + "complement": undefined, + "label": "projects.sorting.name", + "short": undefined, + "value": "name", + }, + Object { + "complement": undefined, + "label": "projects.sorting.analysis_date", + "short": undefined, + "value": "analysis_date", + }, + Object { + "complement": undefined, + "label": "projects.sorting.reliability", + "short": undefined, + "value": "reliability", + }, + Object { + "complement": undefined, + "label": "projects.sorting.security", + "short": undefined, + "value": "security", + }, + Object { + "complement": undefined, + "label": "projects.sorting.maintainability", + "short": undefined, + "value": "maintainability", + }, + Object { + "complement": undefined, + "label": "projects.sorting.coverage", + "short": undefined, + "value": "coverage", + }, + Object { + "complement": undefined, + "label": "projects.sorting.duplications", + "short": undefined, + "value": "duplications", + }, + Object { + "complement": undefined, + "label": "projects.sorting.size", + "short": undefined, + "value": "size", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="vulnerability" + valueComponent={[Function]} + valueKey="value" + /> + <Tooltip + overlay="projects.sort_descending" + placement="bottom" + > + <a + className="spacer-left button-icon" + href="#" + onClick={[Function]} + > + <SortDescIcon + className="little-spacer-top" + /> + </a> + </Tooltip> +</div> +`; + +exports[`should render correctly for leak view 1`] = ` +<div> + <label> + projects.sort_by + : + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + className="little-spacer-left input-large" + clearAllText="Clear all" + clearValueText="Clear value" + clearable={false} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + openAfterFocus={false} + optionComponent={[Function]} + options={ + Array [ + Object { + "complement": undefined, + "label": "projects.sorting.name", + "short": undefined, + "value": "name", + }, + Object { + "complement": undefined, + "label": "projects.sorting.analysis_date", + "short": undefined, + "value": "analysis_date", + }, + Object { + "complement": "projects.sorting.on_new_code", + "label": "projects.sorting.new_reliability", + "short": "projects.sorting.new_reliability.short", + "value": "new_reliability", + }, + Object { + "complement": "projects.sorting.on_new_code", + "label": "projects.sorting.new_security", + "short": "projects.sorting.new_security.short", + "value": "new_security", + }, + Object { + "complement": "projects.sorting.on_new_code", + "label": "projects.sorting.new_maintainability", + "short": "projects.sorting.new_maintainability.short", + "value": "new_maintainability", + }, + Object { + "complement": "projects.sorting.on_new_code", + "label": "projects.sorting.new_coverage", + "short": "projects.sorting.new_coverage.short", + "value": "new_coverage", + }, + Object { + "complement": "projects.sorting.on_new_lines", + "label": "projects.sorting.new_duplications", + "short": "projects.sorting.new_duplications.short", + "value": "new_duplications", + }, + Object { + "complement": undefined, + "label": "projects.sorting.new_lines", + "short": undefined, + "value": "new_lines", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="new_coverage" + valueComponent={[Function]} + valueKey="value" + /> + <Tooltip + overlay="projects.sort_ascending" + placement="bottom" + > + <a + className="spacer-left button-icon" + href="#" + onClick={[Function]} + > + <SortAscIcon + className="little-spacer-top" + /> + </a> + </Tooltip> +</div> +`; + +exports[`should render correctly for overall view 1`] = ` +<div> + <label> + projects.sort_by + : + </label> + <Select + addLabelText="Add \\"{label}\\"?" + arrowRenderer={[Function]} + autosize={true} + backspaceRemoves={true} + backspaceToRemoveMessage="Press backspace to remove {label}" + className="little-spacer-left input-large" + clearAllText="Clear all" + clearValueText="Clear value" + clearable={false} + delimiter="," + disabled={false} + escapeClearsValue={true} + filterOptions={[Function]} + ignoreAccents={true} + ignoreCase={true} + inputProps={Object {}} + isLoading={false} + joinValues={false} + labelKey="label" + matchPos="any" + matchProp="any" + menuBuffer={0} + menuRenderer={[Function]} + multi={false} + noResultsText="No results found" + onBlurResetsInput={true} + onChange={[Function]} + onCloseResetsInput={true} + openAfterFocus={false} + optionComponent={[Function]} + options={ + Array [ + Object { + "complement": undefined, + "label": "projects.sorting.name", + "short": undefined, + "value": "name", + }, + Object { + "complement": undefined, + "label": "projects.sorting.analysis_date", + "short": undefined, + "value": "analysis_date", + }, + Object { + "complement": undefined, + "label": "projects.sorting.reliability", + "short": undefined, + "value": "reliability", + }, + Object { + "complement": undefined, + "label": "projects.sorting.security", + "short": undefined, + "value": "security", + }, + Object { + "complement": undefined, + "label": "projects.sorting.maintainability", + "short": undefined, + "value": "maintainability", + }, + Object { + "complement": undefined, + "label": "projects.sorting.coverage", + "short": undefined, + "value": "coverage", + }, + Object { + "complement": undefined, + "label": "projects.sorting.duplications", + "short": undefined, + "value": "duplications", + }, + Object { + "complement": undefined, + "label": "projects.sorting.size", + "short": undefined, + "value": "size", + }, + ] + } + pageSize={5} + placeholder="Select..." + required={false} + scrollMenuIntoView={true} + searchable={false} + simpleValue={false} + tabSelectsValue={true} + value="name" + valueComponent={[Function]} + valueKey="value" + /> + <Tooltip + overlay="projects.sort_ascending" + placement="bottom" + > + <a + className="spacer-left button-icon" + href="#" + onClick={[Function]} + > + <SortAscIcon + className="little-spacer-top" + /> + </a> + </Tooltip> +</div> +`; 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 index 827e5e482f8..f4a101535e3 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js @@ -20,7 +20,6 @@ import React from 'react'; import FilterContainer from './FilterContainer'; import FilterHeader from './FilterHeader'; -import SortingFilter from './SortingFilter'; import CoverageRating from '../../../components/ui/CoverageRating'; import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; import { translate } from '../../../helpers/l10n'; @@ -70,17 +69,7 @@ export default class CoverageFilter extends React.PureComponent { organization={this.props.organization} getFacetValueForOption={this.getFacetValueForOption} highlightUnder={1} - header={ - <FilterHeader name={translate('metric_domain.Coverage')}> - <SortingFilter - property={this.props.property} - query={this.props.query} - isFavorite={this.props.isFavorite} - organization={this.props.organization} - sortDesc="right" - /> - </FilterHeader> - } + header={<FilterHeader name={translate('metric_domain.Coverage')} />} /> ); } 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 index dcda2e9bccf..609b3388a05 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js @@ -20,7 +20,6 @@ import React from 'react'; import FilterContainer from './FilterContainer'; import FilterHeader from './FilterHeader'; -import SortingFilter from './SortingFilter'; import DuplicationsRating from '../../../components/ui/DuplicationsRating'; import { getDuplicationsRatingLabel, @@ -73,16 +72,7 @@ export default class DuplicationsFilter extends React.PureComponent { organization={this.props.organization} getFacetValueForOption={this.getFacetValueForOption} highlightUnder={1} - header={ - <FilterHeader name={translate('metric_domain.Duplications')}> - <SortingFilter - property={this.props.property} - query={this.props.query} - isFavorite={this.props.isFavorite} - organization={this.props.organization} - /> - </FilterHeader> - } + header={<FilterHeader name={translate('metric_domain.Duplications')} />} /> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/NewSizeFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.js index c672d6c0ce4..3aa2f889cfc 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/NewSizeFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/NewLinesFilter.js @@ -20,11 +20,10 @@ import React from 'react'; import FilterContainer from './FilterContainer'; import FilterHeader from './FilterHeader'; -import SortingFilter from './SortingFilter'; import { translate } from '../../../helpers/l10n'; import { getSizeRatingLabel } from '../../../helpers/ratings'; -export default class NewSizeFilter extends React.PureComponent { +export default class NewLinesFilter extends React.PureComponent { static propTypes = { className: React.PropTypes.string, query: React.PropTypes.object.isRequired, @@ -68,18 +67,7 @@ export default class NewSizeFilter extends React.PureComponent { organization={this.props.organization} getFacetValueForOption={this.getFacetValueForOption} highlightUnder={1} - header={ - <FilterHeader name={translate('metric_domain.new_size')}> - <SortingFilter - property={this.props.property} - query={this.props.query} - isFavorite={this.props.isFavorite} - organization={this.props.organization} - leftText={translate('biggest')} - rightText={translate('smallest')} - /> - </FilterHeader> - } + header={<FilterHeader name={translate('projects.facets.new_lines')} />} /> ); } 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 index aedd2d966cd..ca46efe6c21 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js +++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js @@ -20,7 +20,6 @@ import React from 'react'; import FilterContainer from './FilterContainer'; import FilterHeader from './FilterHeader'; -import SortingFilter from './SortingFilter'; import SizeRating from '../../../components/ui/SizeRating'; import { translate } from '../../../helpers/l10n'; import { getSizeRatingLabel, getSizeRatingAverageValue } from '../../../helpers/ratings'; @@ -72,18 +71,7 @@ export default class SizeFilter extends React.PureComponent { organization={this.props.organization} getFacetValueForOption={this.getFacetValueForOption} highlightUnder={1} - header={ - <FilterHeader name={translate('metric_domain.Size')}> - <SortingFilter - property={this.props.property} - query={this.props.query} - isFavorite={this.props.isFavorite} - organization={this.props.organization} - leftText={translate('biggest')} - rightText={translate('smallest')} - /> - </FilterHeader> - } + header={<FilterHeader name={translate('metric_domain.Size')} />} /> ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js deleted file mode 100644 index 61f7dfa1262..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import classNames from 'classnames'; -import { Link } from 'react-router'; -import { getFilterUrl } from './utils'; -import { translate } from '../../../helpers/l10n'; - -export default class SortingFilter extends React.PureComponent { - static propTypes = { - property: React.PropTypes.string.isRequired, - query: React.PropTypes.object.isRequired, - isFavorite: React.PropTypes.bool, - organization: React.PropTypes.object, - sortDesc: React.PropTypes.oneOf(['left', 'right']), - leftText: React.PropTypes.string, - rightText: React.PropTypes.string - }; - - static defaultProps = { - sortDesc: 'left' - }; - - isSortActive(side) { - const { sort } = this.props.query; - if (sort && sort[0] === '-') { - return sort.substr(1) === this.props.property && side === this.props.sortDesc; - } else { - return sort === this.props.property && side !== this.props.sortDesc; - } - } - - getLinkClass(side) { - return classNames('button button-small button-grey', { - 'button-active': this.isSortActive(side) - }); - } - - getLinkPath(side) { - if (this.isSortActive(side)) { - return getFilterUrl(this.props, { sort: null }); - } - return getFilterUrl(this.props, { - sort: (this.props.sortDesc === side ? '-' : '') + this.props.property - }); - } - - blurLink(event) { - event.target.blur(); - } - - render() { - const { leftText, rightText } = this.props; - - return ( - <div className="projects-facet-sort"> - <span>{translate('projects.sort_list')}</span> - <div className="spacer-left button-group"> - <Link - onClick={this.blurLink} - className={this.getLinkClass('left')} - to={this.getLinkPath('left')}> - {leftText || translate('worst')} - </Link> - <Link - onClick={this.blurLink} - className={this.getLinkClass('right')} - to={this.getLinkPath('right')}> - {rightText || translate('best')} - </Link> - </div> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SortingFilter-test.js b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SortingFilter-test.js deleted file mode 100644 index a73ffcbadf9..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SortingFilter-test.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import SortingFilter from '../SortingFilter'; - -it('should render with default parameters and empty query', () => { - const wrapper = shallow(<SortingFilter property="foo" query={{}} />); - expect(wrapper).toMatchSnapshot(); - const sortingFilter = wrapper.instance(); - expect(sortingFilter.isSortActive('left')).toBeFalsy(); - expect(sortingFilter.isSortActive('right')).toBeFalsy(); -}); - -it('should render with custom parameters', () => { - const wrapper = shallow( - <SortingFilter property="foo" query={{}} sortDesc="right" leftText="worst" rightText="best" /> - ); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render correctly with matching query', () => { - const wrapper = shallow( - <SortingFilter property="foo" query={{ sort: '-foo', languages: 'php,cpp' }} sortDesc="right" /> - ); - expect(wrapper).toMatchSnapshot(); - const sortingFilter = wrapper.instance(); - expect(sortingFilter.isSortActive('left')).toBeFalsy(); - expect(sortingFilter.isSortActive('right')).toBeTruthy(); -}); - -it('should render correctly with no matching query', () => { - const wrapper = shallow(<SortingFilter property="foo" query={{ sort: 'bar' }} />); - expect(wrapper).toMatchSnapshot(); - const sortingFilter = wrapper.instance(); - expect(sortingFilter.isSortActive('left')).toBeFalsy(); - expect(sortingFilter.isSortActive('right')).toBeFalsy(); -}); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SortingFilter-test.js.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SortingFilter-test.js.snap deleted file mode 100644 index 3854d9fdf16..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SortingFilter-test.js.snap +++ /dev/null @@ -1,186 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly with matching query 1`] = ` -<div - className="projects-facet-sort" -> - <span> - projects.sort_list - </span> - <div - className="spacer-left button-group" - > - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "languages": "php,cpp", - "sort": "foo", - }, - } - } - > - worst - </Link> - <Link - className="button button-small button-grey button-active" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "languages": "php,cpp", - }, - } - } - > - best - </Link> - </div> -</div> -`; - -exports[`should render correctly with no matching query 1`] = ` -<div - className="projects-facet-sort" -> - <span> - projects.sort_list - </span> - <div - className="spacer-left button-group" - > - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "-foo", - }, - } - } - > - worst - </Link> - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "foo", - }, - } - } - > - best - </Link> - </div> -</div> -`; - -exports[`should render with custom parameters 1`] = ` -<div - className="projects-facet-sort" -> - <span> - projects.sort_list - </span> - <div - className="spacer-left button-group" - > - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "foo", - }, - } - } - > - worst - </Link> - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "-foo", - }, - } - } - > - best - </Link> - </div> -</div> -`; - -exports[`should render with default parameters and empty query 1`] = ` -<div - className="projects-facet-sort" -> - <span> - projects.sort_list - </span> - <div - className="spacer-left button-group" - > - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "-foo", - }, - } - } - > - worst - </Link> - <Link - className="button button-small button-grey" - onClick={[Function]} - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/projects", - "query": Object { - "sort": "foo", - }, - } - } - > - best - </Link> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 6786e39d14b..a6c471bef8c 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -39,30 +39,25 @@ border-bottom: 1px solid #e6e6e6; } +.projects-topbar-item { + padding: 0 24px; +} + .projects-topbar-button { box-sizing: border-box; float: right; padding: 11px; + padding-top: 15px; border: none; + border-left: 1px solid #e6e6e6; + border-bottom: 1px solid #e6e6e6; width: 46px; height: 46px; - color: #777; text-align: center; text-decoration: none; cursor: pointer; } -.projects-topbar-button-close { - padding-top: 15px; - border-left: 1px solid #e6e6e6; - border-bottom: 1px solid #e6e6e6; -} - -.projects-topbar-button:hover, .project-topbar-button:focus, .project-topbar-button:active { - color: #444; - outline: none; -} - .projects-sidebar { width: 260px; } diff --git a/server/sonar-web/src/main/js/apps/projects/utils.js b/server/sonar-web/src/main/js/apps/projects/utils.js index a81d6258c14..ab4d157c3a2 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/utils.js @@ -47,6 +47,42 @@ export const saveAll = () => save(LOCALSTORAGE_ALL); export const saveFavorite = () => save(LOCALSTORAGE_FAVORITE); +export const SORTING_METRICS = [ + { value: 'name' }, + { value: 'reliability' }, + { value: 'security' }, + { value: 'maintainability' }, + { value: 'coverage' }, + { value: 'duplications' }, + { value: 'size' } +]; + +export const SORTING_LEAK_METRICS = [ + { value: 'name' }, + { value: 'new_reliability', complement: 'on_new_code' }, + { value: 'new_security', complement: 'on_new_code' }, + { value: 'new_maintainability', complement: 'on_new_code' }, + { value: 'new_coverage', complement: 'on_new_code' }, + { value: 'new_duplications', complement: 'on_new_lines' }, + { value: 'new_lines' } +]; + +export const SORTING_SWITCH = { + name: 'name', + reliability: 'new_reliability', + security: 'new_security', + maintainability: 'new_maintainability', + coverage: 'new_coverage', + duplications: 'new_duplications', + size: 'new_lines', + new_reliability: 'reliability', + new_security: 'security', + new_maintainability: 'maintainability', + new_coverage: 'coverage', + new_duplications: 'duplications', + new_lines: 'size' +}; + export const VIEWS = ['overall', 'leak']; export const VISUALIZATIONS = [ @@ -61,3 +97,8 @@ export const VISUALIZATIONS = [ export const localizeSorting = (sort?: string) => { return translate('projects.sort', sort || 'name'); }; + +export const parseSorting = (sort: string): { sortValue: string, sortDesc: boolean } => { + const desc = sort[0] === '-'; + return { sortValue: desc ? sort.substr(1) : sort, sortDesc: desc }; +}; |