@@ -146,11 +146,9 @@ public class ProjectsPageTest { | |||
@Test | |||
public void should_sort_by_facet() { | |||
ProjectsPage page = nav.openProjects(); | |||
page.getFacetByProperty("duplications") | |||
.sortListDesc(); | |||
page.sortProjects("Duplications"); | |||
page.getProjectByIdx(0).shouldHaveMeasure("duplicated_lines_density", "63.7%"); | |||
page.getFacetByProperty("duplications") | |||
.sortListAsc(); | |||
page.invertSorting(); | |||
page.getProjectByIdx(0).shouldHaveMeasure("duplicated_lines_density", "0.0%"); | |||
} | |||
@@ -48,24 +48,4 @@ public class FacetItem { | |||
selectInput.pressEnter(); | |||
return this; | |||
} | |||
private SelenideElement getSortingButton(String selector) { | |||
ElementsCollection buttons = this.elt.$$(".projects-facet-sort a"); | |||
return buttons.find(new Condition("AttributeMatch") { | |||
@Override | |||
public boolean apply(WebElement webElement) { | |||
return webElement.getAttribute("href").matches(".*sort=" + selector + ".*"); | |||
} | |||
}); | |||
} | |||
public FacetItem sortListDesc() { | |||
this.getSortingButton("-").click(); | |||
return this; | |||
} | |||
public FacetItem sortListAsc() { | |||
this.getSortingButton("[a-zA-Z ]").click(); | |||
return this; | |||
} | |||
} |
@@ -89,4 +89,26 @@ public class ProjectsPage { | |||
searchInput.setValue("").sendKeys(search); | |||
return this; | |||
} | |||
public ProjectsPage sortProjects(String sort) { | |||
SelenideElement topbar = $(".projects-topbar-actions").should(Condition.exist); | |||
if (!topbar.has(Condition.hasClass("open"))){ | |||
$(".js-projects-topbar-open").click(); | |||
} | |||
topbar.should(Condition.hasClass("open")); | |||
SelenideElement sortSelect = topbar.$(".js-projects-sorting-select"); | |||
sortSelect.$(".Select-value").should(Condition.exist).click(); | |||
sortSelect.$(".Select-option[title='" + sort + "']").should(Condition.exist).click(); | |||
return this; | |||
} | |||
public ProjectsPage invertSorting() { | |||
SelenideElement topbar = $(".projects-topbar-actions").should(Condition.exist); | |||
if (!topbar.has(Condition.hasClass("open"))){ | |||
$(".js-projects-topbar-open").click(); | |||
} | |||
topbar.should(Condition.hasClass("open")); | |||
topbar.$(".js-projects-sorting-select a.button-icon").should(Condition.exist).click(); | |||
return this; | |||
} | |||
} |
@@ -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} | |||
/> |
@@ -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> |
@@ -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} /> |
@@ -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" |
@@ -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> |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -97,7 +97,7 @@ exports[`should render \`leak\` view correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
<NewSizeFilter | |||
<NewLinesFilter | |||
isFavorite={false} | |||
property="new_lines" | |||
query={ |
@@ -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> |
@@ -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> | |||
`; |
@@ -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')} />} | |||
/> | |||
); | |||
} |
@@ -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')} />} | |||
/> | |||
); | |||
} |
@@ -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')} />} | |||
/> | |||
); | |||
} |
@@ -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')} />} | |||
/> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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; | |||
} |
@@ -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 }; | |||
}; |
@@ -0,0 +1,40 @@ | |||
/* | |||
* 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'; | |||
type Props = { className?: string, size?: number }; | |||
export default function SortAscIcon({ className, size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
className={className} | |||
xmlns="http://www.w3.org/2000/svg" | |||
viewBox="0 0 16 16" | |||
width={size} | |||
height={size}> | |||
<path | |||
style={{ fill: 'currentColor' }} | |||
d="M6.571 12.857q0 0.107-0.089 0.214l-2.848 2.848q-0.089 0.080-0.205 0.080-0.107 0-0.205-0.080l-2.857-2.857q-0.134-0.143-0.063-0.313 0.071-0.179 0.268-0.179h1.714v-12.286q0-0.125 0.080-0.205t0.205-0.080h1.714q0.125 0 0.205 0.080t0.080 0.205v12.286h1.714q0.125 0 0.205 0.080t0.080 0.205zM16 14v1.714q0 0.125-0.080 0.205t-0.205 0.080h-7.429q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h7.429q0.125 0 0.205 0.080t0.080 0.205zM14.286 9.429v1.714q0 0.125-0.080 0.205t-0.205 0.080h-5.714q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h5.714q0.125 0 0.205 0.080t0.080 0.205zM12.571 4.857v1.714q0 0.125-0.080 0.205t-0.205 0.080h-4q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h4q0.125 0 0.205 0.080t0.080 0.205zM10.857 0.286v1.714q0 0.125-0.080 0.205t-0.205 0.080h-2.286q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h2.286q0.125 0 0.205 0.080t0.080 0.205z" | |||
/> | |||
</svg> | |||
); | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* 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'; | |||
type Props = { className?: string, size?: number }; | |||
export default function SortDescIcon({ className, size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
className={className} | |||
xmlns="http://www.w3.org/2000/svg" | |||
viewBox="0 0 16 16" | |||
width={size} | |||
height={size}> | |||
<path | |||
style={{ fill: 'currentColor' }} | |||
d="M10.857 14v1.714q0 0.125-0.080 0.205t-0.205 0.080h-2.286q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h2.286q0.125 0 0.205 0.080t0.080 0.205zM6.571 12.857q0 0.107-0.089 0.214l-2.848 2.848q-0.089 0.080-0.205 0.080-0.107 0-0.205-0.080l-2.857-2.857q-0.134-0.143-0.063-0.313 0.071-0.179 0.268-0.179h1.714v-12.286q0-0.125 0.080-0.205t0.205-0.080h1.714q0.125 0 0.205 0.080t0.080 0.205v12.286h1.714q0.125 0 0.205 0.080t0.080 0.205zM12.571 9.429v1.714q0 0.125-0.080 0.205t-0.205 0.080h-4q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h4q0.125 0 0.205 0.080t0.080 0.205zM14.286 4.857v1.714q0 0.125-0.080 0.205t-0.205 0.080h-5.714q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h5.714q0.125 0 0.205 0.080t0.080 0.205zM16 0.286v1.714q0 0.125-0.080 0.205t-0.205 0.080h-7.429q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h7.429q0.125 0 0.205 0.080t0.080 0.205z" | |||
/> | |||
</svg> | |||
); | |||
} |
@@ -223,6 +223,11 @@ input[type="submit"].button-grey { | |||
} | |||
} | |||
.button-icon { | |||
border: none; | |||
color: @secondFontColor; | |||
} | |||
.button-small { | |||
height: 20px; | |||
line-height: 18px; |
@@ -865,6 +865,29 @@ projects.last_analysis_on_x=Last analysis on {0} | |||
projects.search=Search by project name or key | |||
projects.sort_list=Sort list by | |||
projects.perspective=Perspective | |||
projects.sort_by=Sort by | |||
projects.sort_ascending=Result sorted in ascending order | |||
projects.sort_descending=Result sorted in descending order | |||
projects.sorting.name=Name (default) | |||
projects.sorting.reliability=Reliability | |||
projects.sorting.security=Security | |||
projects.sorting.maintainability=Maintainability | |||
projects.sorting.coverage=Coverage | |||
projects.sorting.duplications=Duplications | |||
projects.sorting.size=Size | |||
projects.sorting.new_reliability=Reliability on New Code | |||
projects.sorting.new_security=Security on New Code | |||
projects.sorting.new_maintainability=Maintainability on New Code | |||
projects.sorting.new_coverage=Coverage on New Code | |||
projects.sorting.new_duplications=Duplications on New Lines | |||
projects.sorting.new_reliability.short=Reliability | |||
projects.sorting.new_security.short=Security | |||
projects.sorting.new_maintainability.short=Maintainability | |||
projects.sorting.new_coverage.short=Coverage | |||
projects.sorting.new_duplications.short=Duplications | |||
projects.sorting.new_lines=New Lines | |||
projects.sorting.on_new_code=on New Code | |||
projects.sorting.on_new_lines=on New Lines | |||
projects.view_settings=Settings | |||
projects.view.overall=Overall Status | |||
projects.view.leak=Leak | |||
@@ -884,6 +907,7 @@ projects.visualization.duplications.description=See duplications' long-term risk | |||
projects.limited_set_of_projects=Displayed project set limited to the top {0} projects based on current sort: {1}. | |||
projects.facets.quality_gate=Quality Gate | |||
projects.facets.languages=Languages | |||
projects.facets.new_lines=New Lines | |||
projects.facets.tags=Tags | |||
projects.sort.name=by name | |||
projects.sort.reliability=by reliability (best first) | |||
@@ -1939,7 +1963,6 @@ severity.INFO.description=Neither a bug nor a quality flaw. Just a finding. | |||
#------------------------------------------------------------------------------ | |||
metric_domain.Size=Size | |||
metric_domain.new_size=New Lines | |||
metric_domain.Tests=Tests | |||
metric_domain.Integration Tests=Integration Tests | |||
metric_domain.Complexity=Complexity |