@@ -21,7 +21,7 @@ | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
/*:: import type { Component } from './utils'; */ | |||
import FavoriteIcon from '../../../components/common/FavoriteIcon'; | |||
import FavoriteIcon from '../../../components/icons-components/FavoriteIcon'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import ClockIcon from '../../../components/common/ClockIcon'; | |||
import Tooltip from '../../../components/controls/Tooltip'; |
@@ -21,7 +21,6 @@ exports[`should render correctly 1`] = ` | |||
</span> | |||
<Rating | |||
className="spacer-left" | |||
muted={false} | |||
small={true} | |||
value="5.0" | |||
/> |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as utils from '../utils'; | |||
describe('localizeSorting', () => { | |||
it('localizes default sorting', () => { | |||
expect(utils.localizeSorting()).toBe('projects.sort.name'); | |||
}); | |||
it('localizes custom sorting', () => { | |||
expect(utils.localizeSorting('size')).toBe('projects.sort.size'); | |||
}); | |||
}); | |||
describe('parseSorting', () => { | |||
it('parses ascending', () => { | |||
expect(utils.parseSorting('size')).toEqual({ sortDesc: false, sortValue: 'size' }); | |||
}); | |||
it('parses descending', () => { | |||
expect(utils.parseSorting('-size')).toEqual({ sortDesc: true, sortValue: 'size' }); | |||
}); | |||
}); |
@@ -17,8 +17,8 @@ | |||
* 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 * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import Helmet from 'react-helmet'; | |||
import PageHeaderContainer from './PageHeaderContainer'; | |||
import ProjectsListContainer from './ProjectsListContainer'; | |||
@@ -29,32 +29,27 @@ import { parseUrlQuery } from '../store/utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import * as utils from '../utils'; | |||
import * as storage from '../../../helpers/storage'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
import '../styles.css'; | |||
/*:: | |||
type Props = {| | |||
isFavorite: boolean, | |||
location: { pathname: string, query: RawQuery }, | |||
fetchProjects: (query: string, isFavorite: boolean, organization?: {}) => Promise<*>, | |||
organization?: { key: string }, | |||
router: { | |||
push: ({ pathname: string, query?: {} }) => void, | |||
replace: ({ pathname: string, query?: {} }) => void | |||
}, | |||
currentUser?: { isLoggedIn: boolean } | |||
|}; | |||
*/ | |||
/*:: | |||
type State = { | |||
query: RawQuery | |||
}; | |||
*/ | |||
export default class AllProjects extends React.PureComponent { | |||
/*:: props: Props; */ | |||
state /*: State */ = { query: {} }; | |||
interface Props { | |||
isFavorite: boolean; | |||
location: { pathname: string; query: RawQuery }; | |||
fetchProjects: (query: RawQuery, isFavorite: boolean, organization?: {}) => Promise<any>; | |||
organization?: { key: string }; | |||
currentUser?: { isLoggedIn: boolean }; | |||
} | |||
interface State { | |||
query: RawQuery; | |||
} | |||
export default class AllProjects extends React.PureComponent<Props, State> { | |||
state: State = { query: {} }; | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
componentDidMount() { | |||
this.handleQueryChange(true); | |||
@@ -62,7 +57,7 @@ export default class AllProjects extends React.PureComponent { | |||
footer && footer.classList.add('page-footer-with-sidebar'); | |||
} | |||
componentDidUpdate(prevProps /*: Props */) { | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.location.query !== this.props.location.query) { | |||
this.handleQueryChange(false); | |||
} | |||
@@ -82,23 +77,29 @@ export default class AllProjects extends React.PureComponent { | |||
isFiltered = () => Object.keys(this.state.query).some(key => this.state.query[key] != null); | |||
getSavedOptions = () => { | |||
const options = {}; | |||
const options: { | |||
sort?: string; | |||
view?: string; | |||
visualization?: string; | |||
} = {}; | |||
if (storage.getSort()) { | |||
options.sort = storage.getSort(); | |||
options.sort = storage.getSort() || undefined; | |||
} | |||
if (storage.getView()) { | |||
options.view = storage.getView(); | |||
options.view = storage.getView() || undefined; | |||
} | |||
if (storage.getVisualization()) { | |||
options.visualization = storage.getVisualization(); | |||
options.visualization = storage.getVisualization() || undefined; | |||
} | |||
return options; | |||
}; | |||
handlePerspectiveChange = ( | |||
{ view, visualization } /*: { view: string, visualization?: string } */ | |||
) => { | |||
const query /*: { view: ?string, visualization: ?string, sort?: ?string } */ = { | |||
handlePerspectiveChange = ({ view, visualization }: { view: string; visualization?: string }) => { | |||
const query: { | |||
view: string | undefined; | |||
visualization: string | undefined; | |||
sort?: string | undefined; | |||
} = { | |||
view: view === 'overall' ? undefined : view, | |||
visualization | |||
}; | |||
@@ -110,7 +111,7 @@ export default class AllProjects extends React.PureComponent { | |||
query.sort = (sort.sortDesc ? '-' : '') + utils.SORTING_SWITCH[sort.sortValue]; | |||
} | |||
} | |||
this.props.router.push({ pathname: this.props.location.pathname, query }); | |||
this.context.router.push({ pathname: this.props.location.pathname, query }); | |||
} else { | |||
this.updateLocationQuery(query); | |||
} | |||
@@ -120,28 +121,28 @@ export default class AllProjects extends React.PureComponent { | |||
storage.saveVisualization(visualization); | |||
}; | |||
handleSortChange = (sort /*: string */, desc /*: boolean */) => { | |||
handleSortChange = (sort: string, desc: boolean) => { | |||
const asString = (desc ? '-' : '') + sort; | |||
this.updateLocationQuery({ sort: asString }); | |||
storage.saveSort(asString); | |||
}; | |||
handleQueryChange(initialMount /*: boolean */) { | |||
handleQueryChange(initialMount: boolean) { | |||
const query = parseUrlQuery(this.props.location.query); | |||
const savedOptions = this.getSavedOptions(); | |||
const savedOptionsSet = savedOptions.sort || savedOptions.view || savedOptions.visualization; | |||
// if there is no filter, but there are saved preferences in the localStorage | |||
if (initialMount && !this.isFiltered() && savedOptionsSet) { | |||
this.props.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); | |||
this.context.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); | |||
} else { | |||
this.setState({ query }); | |||
this.props.fetchProjects(query, this.props.isFavorite, this.props.organization); | |||
} | |||
} | |||
updateLocationQuery = (newQuery /*: { [string]: ?string } */) => { | |||
this.props.router.push({ | |||
updateLocationQuery = (newQuery: RawQuery) => { | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...this.props.location.query, |
@@ -18,8 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import AllProjects from './AllProjects'; | |||
import { fetchProjects } from '../store/actions'; | |||
export default connect(null, { fetchProjects })(withRouter(AllProjects)); | |||
export default connect<null, any, any>(null, { fetchProjects })(AllProjects); |
@@ -17,38 +17,30 @@ | |||
* 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 * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import * as PropTypes from 'prop-types'; | |||
import AllProjectsContainer from './AllProjectsContainer'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { isFavoriteSet, isAllSet } from '../../../helpers/storage'; | |||
import { searchProjects } from '../../../api/components'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
/*:: | |||
type Props = { | |||
currentUser: { isLoggedIn: boolean }, | |||
location: { query: {} }, | |||
router: { | |||
replace: (location: { pathname?: string, query?: RawQuery }) => void | |||
} | |||
}; | |||
*/ | |||
interface Props { | |||
currentUser: { isLoggedIn: boolean }; | |||
location: { query: { [x: string]: string } }; | |||
} | |||
/*:: | |||
type State = { | |||
shouldBeRedirected?: boolean, | |||
shouldForceSorting?: string | |||
}; | |||
*/ | |||
interface State { | |||
shouldBeRedirected?: boolean; | |||
shouldForceSorting?: string; | |||
} | |||
class DefaultPageSelector extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
class DefaultPageSelector extends React.PureComponent<Props, State> { | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
constructor(props /*: Props */) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = {}; | |||
} | |||
@@ -57,13 +49,13 @@ class DefaultPageSelector extends React.PureComponent { | |||
this.defineIfShouldBeRedirected(); | |||
} | |||
componentDidUpdate(prevProps /*: Props */) { | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.location !== this.props.location) { | |||
this.defineIfShouldBeRedirected(); | |||
} else if (this.state.shouldBeRedirected === true) { | |||
this.props.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); | |||
this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); | |||
} else if (this.state.shouldForceSorting != null) { | |||
this.props.router.replace({ | |||
this.context.router.replace({ | |||
...this.props.location, | |||
query: { | |||
...this.props.location.query, | |||
@@ -117,8 +109,10 @@ class DefaultPageSelector extends React.PureComponent { | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps)(withRouter(DefaultPageSelector)); | |||
export default connect<any, any, any>(mapStateToProps)(DefaultPageSelector); | |||
export const UnconnectedDefaultPageSelector = DefaultPageSelector; |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function EmptyInstance() { |
@@ -17,26 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import { IndexLink, Link } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { saveAll, saveFavorite } from '../../../helpers/storage'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
/*:: | |||
type Props = { | |||
user: { | |||
isLoggedIn?: boolean | |||
}, | |||
organization?: { key: string }, | |||
query: RawQuery | |||
}; | |||
*/ | |||
export default class FavoriteFilter extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
user: { isLoggedIn?: boolean }; | |||
organization?: { key: string }; | |||
query: RawQuery; | |||
} | |||
export default class FavoriteFilter extends React.PureComponent<Props> { | |||
handleSaveFavorite = () => { | |||
if (!this.props.organization) { | |||
saveFavorite(); |
@@ -21,8 +21,8 @@ import { connect } from 'react-redux'; | |||
import FavoriteFilter from './FavoriteFilter'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
const mapStateToProps = state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
user: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps)(FavoriteFilter); | |||
export default connect<any, any, any>(mapStateToProps)(FavoriteFilter); |
@@ -18,14 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import AllProjects from './AllProjects'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { fetchProjects } from '../store/actions'; | |||
const mapStateToProps = state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
isFavorite: true, | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps, { fetchProjects })(withRouter(AllProjects)); | |||
export default connect<any, any, any>(mapStateToProps, { fetchProjects })(AllProjects); |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -17,42 +17,45 @@ | |||
* 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import SearchFilterContainer from '../filters/SearchFilterContainer'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import PerspectiveSelect from './PerspectiveSelect'; | |||
import ProjectsSortingSelect from './ProjectsSortingSelect'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
/*:: | |||
type Props = {| | |||
currentUser?: { isLoggedIn: boolean }, | |||
isFavorite?: boolean, | |||
onPerspectiveChange: ({ view: string, visualization?: string }) => void, | |||
organization?: { key: string }, | |||
projects: Array<*>, | |||
projectsAppState: { loading: boolean, total?: number }, | |||
query: RawQuery, | |||
onSortChange: (sort: string, desc: boolean) => void, | |||
selectedSort: string, | |||
view: string, | |||
visualization?: string | |||
|}; | |||
*/ | |||
interface Props { | |||
currentUser?: { isLoggedIn: boolean }; | |||
isFavorite?: boolean; | |||
onPerspectiveChange: (x: { view: string; visualization?: string }) => void; | |||
organization?: { key: string }; | |||
projects: Array<any>; | |||
projectsAppState: { loading: boolean; total?: number }; | |||
query: RawQuery; | |||
onSortChange: (sort: string, desc: boolean) => void; | |||
selectedSort: string; | |||
view: string; | |||
visualization?: string; | |||
} | |||
export default function PageHeader(props: Props) { | |||
const { projectsAppState, projects, currentUser, view } = props; | |||
const limitReached = | |||
projects != null && projectsAppState.total != null && projects.length < projectsAppState.total; | |||
const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date'; | |||
return ( | |||
<header className="page-header projects-topbar-items"> | |||
<PerspectiveSelect | |||
className="projects-topbar-item js-projects-perspective-select" | |||
onChange={props.onPerspectiveChange} | |||
view={props.view} | |||
visualization={props.visualization} | |||
/> | |||
export default function PageHeader(props /*: Props */) { | |||
const renderSortingSelect = () => { | |||
const { projectsAppState, projects, currentUser, view } = props; | |||
const limitReached = | |||
projects != null && | |||
projectsAppState.total != null && | |||
projects.length < projectsAppState.total; | |||
const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date'; | |||
if (view === 'visualizations' && !limitReached) { | |||
return ( | |||
{view === 'visualizations' && !limitReached ? ( | |||
<Tooltip overlay={translate('projects.sort.disabled')}> | |||
<div className="projects-topbar-item disabled"> | |||
<ProjectsSortingSelect | |||
@@ -64,29 +67,15 @@ export default function PageHeader(props /*: Props */) { | |||
/> | |||
</div> | |||
</Tooltip> | |||
); | |||
} | |||
return ( | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption={defaultOption} | |||
onChange={props.onSortChange} | |||
selectedSort={props.selectedSort} | |||
view={props.view} | |||
/> | |||
); | |||
}; | |||
return ( | |||
<header className="page-header projects-topbar-items"> | |||
<PerspectiveSelect | |||
className="projects-topbar-item js-projects-perspective-select" | |||
onChange={props.onPerspectiveChange} | |||
view={props.view} | |||
visualization={props.visualization} | |||
/> | |||
{renderSortingSelect()} | |||
) : ( | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption={defaultOption} | |||
onChange={props.onSortChange} | |||
selectedSort={props.selectedSort} | |||
view={props.view} | |||
/> | |||
)} | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" |
@@ -21,9 +21,9 @@ import { connect } from 'react-redux'; | |||
import PageHeader from './PageHeader'; | |||
import { getProjects, getProjectsAppState } from '../../../store/rootReducer'; | |||
const mapStateToProps = state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
projects: getProjects(state), | |||
projectsAppState: getProjectsAppState(state) | |||
}); | |||
export default connect(mapStateToProps)(PageHeader); | |||
export default connect<any, any, any>(mapStateToProps)(PageHeader); |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import FavoriteFilterContainer from './FavoriteFilterContainer'; | |||
import LanguagesFilterContainer from '../filters/LanguagesFilterContainer'; | |||
@@ -37,21 +36,18 @@ import SecurityFilter from '../filters/SecurityFilter'; | |||
import SizeFilter from '../filters/SizeFilter'; | |||
import TagsFilterContainer from '../filters/TagsFilterContainer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
/*:: | |||
type Props = { | |||
isFavorite: boolean, | |||
organization?: { key: string }, | |||
query: RawQuery, | |||
view: string, | |||
visualization: string | |||
}; | |||
*/ | |||
interface Props { | |||
isFavorite: boolean; | |||
organization?: { key: string }; | |||
query: RawQuery; | |||
view: string; | |||
visualization: string; | |||
} | |||
export default function PageSidebar( | |||
{ query, isFavorite, organization, view, visualization } /*: Props */ | |||
) { | |||
export default function PageSidebar(props: Props) { | |||
const { query, isFavorite, organization, view, visualization } = props; | |||
const isFiltered = Object.keys(query) | |||
.filter(key => !['view', 'visualization', 'sort'].includes(key)) | |||
.some(key => query[key] != null); | |||
@@ -60,12 +56,11 @@ export default function PageSidebar( | |||
const pathname = basePathName + (isFavorite ? '/favorite' : ''); | |||
const facetProps = { query, isFavorite, organization }; | |||
let linkQuery; | |||
let linkQuery: RawQuery | undefined = undefined; | |||
if (view !== 'overall') { | |||
linkQuery = { view }; | |||
if (view === 'visualizations') { | |||
// $FlowFixMe | |||
linkQuery.visualization = visualization; | |||
} | |||
} |
@@ -17,33 +17,32 @@ | |||
* 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 PerspectiveSelectOption from './PerspectiveSelectOption'; | |||
import * as React from 'react'; | |||
import * as Select from 'react-select'; | |||
import PerspectiveSelectOption, { Option } from './PerspectiveSelectOption'; | |||
import { translate } from '../../../helpers/l10n'; | |||
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 | |||
|}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
onChange: (x: { view: string; visualization?: string }) => void; | |||
view: string; | |||
visualization?: string; | |||
} | |||
export default class PerspectiveSelect extends React.PureComponent { | |||
/*:: options: Array<Option>; */ | |||
/*:: props: Props; */ | |||
export default class PerspectiveSelect extends React.PureComponent<Props> { | |||
handleChange = (option: Option) => { | |||
if (option.type === 'view') { | |||
this.props.onChange({ view: option.value }); | |||
} else if (option.type === 'visualization') { | |||
this.props.onChange({ view: 'visualizations', visualization: option.value }); | |||
} | |||
}; | |||
constructor(props /*: Props */) { | |||
super(props); | |||
this.options = [ | |||
render() { | |||
const { view, visualization } = this.props; | |||
const perspective = view === 'visualizations' ? visualization : view; | |||
const options = [ | |||
...VIEWS.map(opt => ({ | |||
type: 'view', | |||
value: opt, | |||
@@ -55,19 +54,6 @@ export default class PerspectiveSelect extends React.PureComponent { | |||
label: translate('projects.visualization', opt) | |||
})) | |||
]; | |||
} | |||
handleChange = (option /*: Option */) => { | |||
if (option.type === 'view') { | |||
this.props.onChange({ view: option.value }); | |||
} else if (option.type === 'visualization') { | |||
this.props.onChange({ view: 'visualizations', visualization: option.value }); | |||
} | |||
}; | |||
render() { | |||
const { view, visualization } = this.props; | |||
const perspective = view === 'visualizations' ? visualization : view; | |||
return ( | |||
<div className={this.props.className}> | |||
<label>{translate('projects.perspective')}:</label> | |||
@@ -76,7 +62,7 @@ export default class PerspectiveSelect extends React.PureComponent { | |||
clearable={false} | |||
onChange={this.handleChange} | |||
optionComponent={PerspectiveSelectOption} | |||
options={this.options} | |||
options={options} | |||
searchable={false} | |||
value={perspective} | |||
/> |
@@ -17,37 +17,37 @@ | |||
* 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 * as React from 'react'; | |||
import BubblesIcon from '../../../components/icons-components/BubblesIcon'; | |||
import ListIcon from '../../../components/icons-components/ListIcon'; | |||
/*:: import type { Option } from './PerspectiveSelect'; */ | |||
/*:: | |||
type Props = { | |||
option: Option, | |||
children?: Element | Text, | |||
className?: string, | |||
isFocused?: boolean, | |||
onFocus: (Option, MouseEvent) => void, | |||
onSelect: (Option, MouseEvent) => void | |||
}; | |||
*/ | |||
export interface Option { | |||
label: string; | |||
type: string; | |||
value: string; | |||
} | |||
export default class PerspectiveSelectOption extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
option: Option; | |||
children?: React.ReactNode; | |||
className?: string; | |||
isFocused?: boolean; | |||
onFocus: (option: Option, event: React.SyntheticEvent<HTMLElement>) => void; | |||
onSelect: (option: Option, event: React.SyntheticEvent<HTMLElement>) => void; | |||
} | |||
handleMouseDown = (event /*: MouseEvent */) => { | |||
export default class PerspectiveSelectOption extends React.PureComponent<Props> { | |||
handleMouseDown = (event: React.SyntheticEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onSelect(this.props.option, event); | |||
}; | |||
handleMouseEnter = (event /*: MouseEvent */) => { | |||
handleMouseEnter = (event: React.SyntheticEvent<HTMLElement>) => { | |||
this.props.onFocus(this.props.option, event); | |||
}; | |||
handleMouseMove = (event /*: MouseEvent */) => { | |||
handleMouseMove = (event: React.SyntheticEvent<HTMLElement>) => { | |||
if (this.props.isFocused) { | |||
return; | |||
} |
@@ -17,20 +17,38 @@ | |||
* 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 * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import ProjectCardLeak from './ProjectCardLeak'; | |||
import ProjectCardOverall from './ProjectCardOverall'; | |||
import { getComponent, getComponentMeasures } from '../../../store/rootReducer'; | |||
function ProjectCard(props /*: { type?: string } */) { | |||
interface Props { | |||
measures?: { [key: string]: string }; | |||
organization?: { key: string }; | |||
project?: { | |||
analysisDate?: string; | |||
key: string; | |||
leakPeriodDate?: string; | |||
name: string; | |||
tags: Array<string>; | |||
isFavorite?: boolean; | |||
organization?: string; | |||
visibility?: string; | |||
}; | |||
type?: string; | |||
} | |||
function ProjectCard(props: Props) { | |||
if (props.type === 'leak') { | |||
return <ProjectCardLeak {...props} />; | |||
} | |||
return <ProjectCardOverall {...props} />; | |||
} | |||
export default connect((state, ownProps) => ({ | |||
const mapStateToProps = (state: any, ownProps: any) => ({ | |||
project: getComponent(state, ownProps.projectKey), | |||
measures: getComponentMeasures(state, ownProps.projectKey) | |||
}))(ProjectCard); | |||
}); | |||
export default connect(mapStateToProps)(ProjectCard); |
@@ -1,75 +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 { sortBy } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getLanguages } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class ProjectCardLanguages extends React.PureComponent { | |||
getLanguageName(key) { | |||
if (key === '<null>') { | |||
return translate('unknown'); | |||
} | |||
const language = this.props.languages[key]; | |||
return language != null ? language.name : key; | |||
} | |||
render() { | |||
const { distribution } = this.props; | |||
if (distribution == null) { | |||
return null; | |||
} | |||
const parsedLanguages = distribution.split(';').map(item => item.split('=')); | |||
const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => | |||
this.getLanguageName(l[0]) | |||
); | |||
const tooltip = ( | |||
<span> | |||
{finalLanguages.map(language => ( | |||
<span key={language}> | |||
{language} | |||
<br /> | |||
</span> | |||
))} | |||
</span> | |||
); | |||
const languagesText = | |||
finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); | |||
return ( | |||
<div className="project-card-languages"> | |||
<Tooltip placement="bottom" overlay={tooltip}> | |||
<span>{languagesText}</span> | |||
</Tooltip> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
languages: getLanguages(state) | |||
}); | |||
export default connect(mapStateToProps)(ProjectCardLanguages); |
@@ -0,0 +1,73 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Languages { | |||
[key: string]: { key: string; name: string }; | |||
} | |||
interface Props { | |||
distribution?: string; | |||
languages: Languages; | |||
} | |||
export default function ProjectCardLanguages({ distribution, languages }: Props) { | |||
if (distribution == undefined) { | |||
return null; | |||
} | |||
const parsedLanguages = distribution.split(';').map(item => item.split('=')); | |||
const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => | |||
getLanguageName(languages, l[0]) | |||
); | |||
const tooltip = ( | |||
<span> | |||
{finalLanguages.map(language => ( | |||
<span key={language}> | |||
{language} | |||
<br /> | |||
</span> | |||
))} | |||
</span> | |||
); | |||
const languagesText = | |||
finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); | |||
return ( | |||
<div className="project-card-languages"> | |||
<Tooltip placement="bottom" overlay={tooltip}> | |||
<span>{languagesText}</span> | |||
</Tooltip> | |||
</div> | |||
); | |||
} | |||
function getLanguageName(languages: Languages, key: string): string { | |||
if (key === '<null>') { | |||
return translate('unknown'); | |||
} | |||
const language = languages[key]; | |||
return language != null ? language.name : key; | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import ProjectCardLanguages from './ProjectCardLanguages'; | |||
import { getLanguages } from '../../../store/rootReducer'; | |||
const mapStateToProps = (state: any) => ({ | |||
languages: getLanguages(state) | |||
}); | |||
export default connect<any, any, any>(mapStateToProps)(ProjectCardLanguages); |
@@ -17,12 +17,11 @@ | |||
* 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Link } from 'react-router'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter.tsx'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import ProjectCardQualityGate from './ProjectCardQualityGate'; | |||
import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; | |||
import FavoriteContainer from '../../../components/controls/FavoriteContainer'; | |||
@@ -31,37 +30,35 @@ import TagsList from '../../../components/tags/TagsList'; | |||
import PrivateBadge from '../../../components/common/PrivateBadge'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
measures: { [string]: string }, | |||
organization?: { key: string }, | |||
interface Props { | |||
measures?: { [key: string]: string }; | |||
organization?: { key: string }; | |||
project?: { | |||
analysisDate?: string, | |||
key: string, | |||
leakPeriodDate?: string, | |||
name: string, | |||
tags: Array<string>, | |||
isFavorite?: boolean, | |||
organization?: string, | |||
visibility?: boolean | |||
} | |||
}; | |||
*/ | |||
analysisDate?: string; | |||
key: string; | |||
leakPeriodDate?: string; | |||
name: string; | |||
tags: Array<string>; | |||
isFavorite?: boolean; | |||
organization?: string; | |||
visibility?: string; | |||
}; | |||
} | |||
export default function ProjectCardLeak({ measures, organization, project } /*: Props */) { | |||
if (project == null) { | |||
export default function ProjectCardLeak({ measures, organization, project }: Props) { | |||
if (project == undefined) { | |||
return null; | |||
} | |||
const isProjectAnalyzed = project.analysisDate != null; | |||
const isPrivate = project.visibility === 'private'; | |||
const hasLeakPeriodStart = project.leakPeriodDate != null; | |||
const hasLeakPeriodStart = project.leakPeriodDate != undefined; | |||
const hasTags = project.tags.length > 0; | |||
const showOrganization = organization == null && project.organization != null; | |||
const showOrganization = organization == undefined && project.organization != undefined; | |||
// check for particular measures because only some measures can be loaded | |||
// if coming from visualizations tab | |||
const areProjectMeasuresLoaded = measures != null && measures['new_bugs']; | |||
const areProjectMeasuresLoaded = measures != undefined && measures['new_bugs']; | |||
const displayQualityGate = areProjectMeasuresLoaded && isProjectAnalyzed; | |||
const className = classNames('boxed-group', 'project-card', { | |||
@@ -82,7 +79,7 @@ export default function ProjectCardLeak({ measures, organization, project } /*: | |||
)} | |||
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> | |||
</h2> | |||
{displayQualityGate && <ProjectCardQualityGate status={measures['alert_status']} />} | |||
{displayQualityGate && <ProjectCardQualityGate status={measures!['alert_status']} />} | |||
<div className="pull-right text-right"> | |||
{isPrivate && <PrivateBadge className="spacer-left" tooltipPlacement="left" />} | |||
{hasTags && <TagsList tags={project.tags} customClass="spacer-left" />} | |||
@@ -91,7 +88,7 @@ export default function ProjectCardLeak({ measures, organization, project } /*: | |||
hasLeakPeriodStart && ( | |||
<div className="project-card-dates note text-right pull-right"> | |||
{hasLeakPeriodStart && ( | |||
<DateFromNow date={project.leakPeriodDate}> | |||
<DateFromNow date={project.leakPeriodDate!}> | |||
{fromNow => ( | |||
<span className="project-card-leak-date pull-right"> | |||
{translateWithParameters('projects.leak_period_x', fromNow)} | |||
@@ -100,7 +97,7 @@ export default function ProjectCardLeak({ measures, organization, project } /*: | |||
</DateFromNow> | |||
)} | |||
{isProjectAnalyzed && ( | |||
<DateTimeFormatter date={project.analysisDate}> | |||
<DateTimeFormatter date={project.analysisDate!}> | |||
{formattedDate => ( | |||
<span> | |||
{translateWithParameters('projects.last_analysis_on_x', formattedDate)} |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import BugIcon from '../../../components/icons-components/BugIcon'; | |||
import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon'; | |||
@@ -26,14 +25,12 @@ import Rating from '../../../components/ui/Rating'; | |||
import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
measures?: { [string]: string } | |||
}; | |||
*/ | |||
interface Props { | |||
measures?: { [key: string]: string }; | |||
} | |||
export default function ProjectCardLeakMeasures({ measures } /*: Props */) { | |||
if (measures == null) { | |||
export default function ProjectCardLeakMeasures({ measures }: Props) { | |||
if (measures == undefined) { | |||
return null; | |||
} | |||
@@ -17,9 +17,8 @@ | |||
* 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 classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Link } from 'react-router'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import ProjectCardQualityGate from './ProjectCardQualityGate'; | |||
@@ -30,36 +29,36 @@ import TagsList from '../../../components/tags/TagsList'; | |||
import PrivateBadge from '../../../components/common/PrivateBadge'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
measures: { [string]: string }, | |||
organization?: { key: string }, | |||
interface Props { | |||
measures?: { [key: string]: string }; | |||
organization?: { key: string }; | |||
project?: { | |||
analysisDate?: string, | |||
key: string, | |||
name: string, | |||
tags: Array<string>, | |||
isFavorite?: boolean, | |||
organization?: string, | |||
visibility?: boolean | |||
} | |||
}; | |||
*/ | |||
analysisDate?: string; | |||
key: string; | |||
name: string; | |||
tags: Array<string>; | |||
isFavorite?: boolean; | |||
organization?: string; | |||
visibility?: string; | |||
}; | |||
} | |||
export default function ProjectCardOverall({ measures, organization, project } /*: Props */) { | |||
if (project == null) { | |||
export default function ProjectCardOverall({ measures, organization, project }: Props) { | |||
if (project == undefined) { | |||
return null; | |||
} | |||
const isProjectAnalyzed = project.analysisDate != null; | |||
const isProjectAnalyzed = project.analysisDate != undefined; | |||
const isPrivate = project.visibility === 'private'; | |||
const hasTags = project.tags.length > 0; | |||
const showOrganization = organization == null && project.organization != null; | |||
const showOrganization = organization == undefined && project.organization != undefined; | |||
// check for particular measures because only some measures can be loaded | |||
// if coming from visualizations tab | |||
const areProjectMeasuresLoaded = | |||
measures != null && measures['reliability_rating'] != null && measures['sqale_rating'] != null; | |||
measures != undefined && | |||
measures['reliability_rating'] != undefined && | |||
measures['sqale_rating'] != undefined; | |||
const displayQualityGate = areProjectMeasuresLoaded && isProjectAnalyzed; | |||
const className = classNames('boxed-group', 'project-card', { | |||
@@ -69,7 +68,7 @@ export default function ProjectCardOverall({ measures, organization, project } / | |||
return ( | |||
<div data-key={project.key} className={className}> | |||
<div className="boxed-group-header clearfix"> | |||
{project.isFavorite != null && ( | |||
{project.isFavorite != undefined && ( | |||
<FavoriteContainer className="spacer-right" componentKey={project.key} /> | |||
)} | |||
<h2 className="project-card-name"> | |||
@@ -80,12 +79,12 @@ export default function ProjectCardOverall({ measures, organization, project } / | |||
)} | |||
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> | |||
</h2> | |||
{displayQualityGate && <ProjectCardQualityGate status={measures['alert_status']} />} | |||
{displayQualityGate && <ProjectCardQualityGate status={measures!['alert_status']} />} | |||
<div className="pull-right text-right"> | |||
{isPrivate && <PrivateBadge className="spacer-left" tooltipPlacement="left" />} | |||
{hasTags && <TagsList tags={project.tags} customClass="spacer-left" />} | |||
</div> | |||
{isProjectAnalyzed && ( | |||
{project.analysisDate && ( | |||
<div className="project-card-dates note text-right"> | |||
<DateTimeFormatter date={project.analysisDate}> | |||
{formattedDate => ( |
@@ -17,9 +17,8 @@ | |||
* 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 ProjectCardLanguages from './ProjectCardLanguages'; | |||
import * as React from 'react'; | |||
import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
@@ -27,14 +26,12 @@ import DuplicationsRating from '../../../components/ui/DuplicationsRating'; | |||
import SizeRating from '../../../components/ui/SizeRating'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
measures?: { [string]: string } | |||
}; | |||
*/ | |||
interface Props { | |||
measures?: { [key: string]: string }; | |||
} | |||
export default function ProjectCardOverallMeasures({ measures } /*: Props */) { | |||
if (measures == null) { | |||
export default function ProjectCardOverallMeasures({ measures }: Props) { | |||
if (measures == undefined) { | |||
return null; | |||
} | |||
@@ -128,7 +125,9 @@ export default function ProjectCardOverallMeasures({ measures } /*: Props */) { | |||
/> | |||
</div> | |||
<div className="project-card-measure-label"> | |||
<ProjectCardLanguages distribution={measures['ncloc_language_distribution']} /> | |||
<ProjectCardLanguagesContainer | |||
distribution={measures['ncloc_language_distribution']} | |||
/> | |||
</div> | |||
</div> | |||
</div> |
@@ -17,14 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import Level from '../../../components/ui/Level'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
export default function ProjectCardQualityGate({ status } /*: { status?: string } */) { | |||
interface Props { | |||
status?: string; | |||
} | |||
export default function ProjectCardQualityGate({ status }: Props) { | |||
if (!status) { | |||
return null; | |||
} |
@@ -17,26 +17,21 @@ | |||
* 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 * as React from 'react'; | |||
import ProjectCardContainer from './ProjectCardContainer'; | |||
import NoFavoriteProjects from './NoFavoriteProjects'; | |||
import EmptyInstance from './EmptyInstance'; | |||
import EmptySearch from '../../../components/common/EmptySearch'; | |||
/*:: | |||
type Props = { | |||
projects?: Array<string>, | |||
isFavorite: boolean, | |||
isFiltered: boolean, | |||
organization?: { key: string }, | |||
cardType?: string | |||
}; | |||
*/ | |||
export default class ProjectsList extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
cardType?: string; | |||
isFavorite: boolean; | |||
isFiltered: boolean; | |||
organization?: { key: string }; | |||
projects?: string[]; | |||
} | |||
export default class ProjectsList extends React.PureComponent<Props> { | |||
renderNoProjects() { | |||
if (this.props.isFavorite && !this.props.isFiltered) { | |||
return <NoFavoriteProjects />; | |||
@@ -50,7 +45,7 @@ export default class ProjectsList extends React.PureComponent { | |||
render() { | |||
const { projects } = this.props; | |||
if (projects == null) { | |||
if (projects == undefined) { | |||
return null; | |||
} | |||
@@ -21,7 +21,9 @@ import { connect } from 'react-redux'; | |||
import ProjectsList from './ProjectsList'; | |||
import { getProjects, getProjectsAppState } from '../../../store/rootReducer'; | |||
export default connect(state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
projects: getProjects(state), | |||
total: getProjectsAppState(state).total | |||
}))(ProjectsList); | |||
}); | |||
export default connect<any, any, any>(mapStateToProps)(ProjectsList); |
@@ -20,9 +20,9 @@ | |||
import { connect } from 'react-redux'; | |||
import { getProjects, getProjectsAppState } from '../../../store/rootReducer'; | |||
import { fetchMoreProjects } from '../store/actions'; | |||
import ProjectsListFooter from './ProjectsListFooter'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
const mapStateToProps = state => { | |||
const mapStateToProps = (state: any) => { | |||
const projects = getProjects(state); | |||
const appState = getProjectsAppState(state); | |||
return { | |||
@@ -32,9 +32,9 @@ const mapStateToProps = state => { | |||
}; | |||
}; | |||
const mapDispatchToProps = (dispatch, ownProps) => ({ | |||
const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ | |||
loadMore: () => | |||
dispatch(fetchMoreProjects(ownProps.query, ownProps.isFavorite, ownProps.organization)) | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectsListFooter); | |||
export default connect(mapStateToProps, mapDispatchToProps)(ListFooter); |
@@ -17,75 +17,51 @@ | |||
* 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 * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import Select from 'react-select'; | |||
import ProjectsSortingSelectOption from './ProjectsSortingSelectOption'; | |||
import * as Select from 'react-select'; | |||
import ProjectsSortingSelectOption, { Option } 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, class?: string, short?: string }; | |||
*/ | |||
/*:: | |||
type Props = { | |||
className?: string, | |||
onChange: (sort: string, desc: boolean) => void, | |||
selectedSort: string, | |||
view: string, | |||
defaultOption: 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); | |||
} | |||
interface Props { | |||
className?: string; | |||
defaultOption: string; | |||
onChange: (sort: string, desc: boolean) => void; | |||
selectedSort: string; | |||
view: string; | |||
} | |||
componentDidUpdate(prevProps /*: Props */) { | |||
if (prevProps.selectedSort !== this.props.selectedSort) { | |||
this.setState(parseSorting(this.props.selectedSort)); | |||
} | |||
} | |||
export default class ProjectsSortingSelect extends React.PureComponent<Props> { | |||
getSorting = () => parseSorting(this.props.selectedSort); | |||
getOptions = () => { | |||
const sortMetrics = this.props.view === 'leak' ? SORTING_LEAK_METRICS : SORTING_METRICS; | |||
return sortBy(sortMetrics, opt => (opt.value === this.props.defaultOption ? 0 : 1)).map(( | |||
opt /*: { value: string, class?: string } */ | |||
) => ({ | |||
value: opt.value, | |||
label: translate('projects.sorting', opt.value), | |||
class: opt.class | |||
return sortBy( | |||
sortMetrics, | |||
option => (option.value === this.props.defaultOption ? 0 : 1) | |||
).map(option => ({ | |||
value: option.value, | |||
label: translate('projects.sorting', option.value), | |||
class: option.class | |||
})); | |||
}; | |||
handleDescToggle = (evt /*: Event & { currentTarget: HTMLElement } */) => { | |||
evt.preventDefault(); | |||
evt.currentTarget.blur(); | |||
this.props.onChange(this.state.sortValue, !this.state.sortDesc); | |||
handleDescToggle = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
const sorting = this.getSorting(); | |||
this.props.onChange(sorting.sortValue, !sorting.sortDesc); | |||
}; | |||
handleSortChange = (option /*: Option */) => | |||
this.props.onChange(option.value, this.state.sortDesc); | |||
handleSortChange = (option: Option) => | |||
this.props.onChange(option.value, this.getSorting().sortDesc); | |||
render() { | |||
const { sortDesc } = this.state; | |||
const { sortDesc, sortValue } = this.getSorting(); | |||
return ( | |||
<div className={this.props.className}> | |||
@@ -97,7 +73,7 @@ export default class ProjectsSortingSelect extends React.PureComponent { | |||
optionComponent={ProjectsSortingSelectOption} | |||
options={this.getOptions()} | |||
searchable={false} | |||
value={this.state.sortValue} | |||
value={sortValue} | |||
/> | |||
<Tooltip | |||
overlay={ |
@@ -17,36 +17,37 @@ | |||
* 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 classNames from 'classnames'; | |||
/*:: import type { Option } from './ProjectsSortingSelect'; */ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
/*:: | |||
type Props = { | |||
option: Option, | |||
children?: Element | Text, | |||
className?: string, | |||
isFocused?: boolean, | |||
onFocus: (Option, MouseEvent) => void, | |||
onSelect: (Option, MouseEvent) => void | |||
}; | |||
*/ | |||
export interface Option { | |||
label: string; | |||
value: string; | |||
class?: string; | |||
short?: string; | |||
} | |||
export default class ProjectsSortingSelectOption extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
option: Option; | |||
children?: React.ReactNode; | |||
className?: string; | |||
isFocused?: boolean; | |||
onFocus: (option: Option, event: React.SyntheticEvent<HTMLElement>) => void; | |||
onSelect: (option: Option, event: React.SyntheticEvent<HTMLElement>) => void; | |||
} | |||
handleMouseDown = (event /*: MouseEvent */) => { | |||
export default class ProjectsSortingSelectOption extends React.PureComponent<Props> { | |||
handleMouseDown = (event: React.SyntheticEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onSelect(this.props.option, event); | |||
}; | |||
handleMouseEnter = (event /*: MouseEvent */) => { | |||
handleMouseEnter = (event: React.SyntheticEvent<HTMLElement>) => { | |||
this.props.onFocus(this.props.option, event); | |||
}; | |||
handleMouseMove = (event /*: MouseEvent */) => { | |||
handleMouseMove = (event: React.SyntheticEvent<HTMLElement>) => { | |||
if (this.props.isFocused) { | |||
return; | |||
} |
@@ -0,0 +1,180 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
jest.mock('../ProjectsListContainer', () => ({ | |||
default: function ProjectsListContainer() { | |||
return null; | |||
} | |||
})); | |||
jest.mock('../ProjectsListFooterContainer', () => ({ | |||
default: function ProjectsListFooterContainer() { | |||
return null; | |||
} | |||
})); | |||
jest.mock('../PageHeaderContainer', () => ({ | |||
default: function PageHeaderContainer() { | |||
return null; | |||
} | |||
})); | |||
jest.mock('../PageSidebar', () => ({ | |||
default: function PageSidebar() { | |||
return null; | |||
} | |||
})); | |||
jest.mock('../../../../helpers/storage', () => ({ | |||
getSort: () => null, | |||
getView: jest.fn(() => null), | |||
getVisualization: () => null, | |||
saveSort: jest.fn(), | |||
saveView: jest.fn(), | |||
saveVisualization: jest.fn() | |||
})); | |||
import * as React from 'react'; | |||
import { mount, shallow } from 'enzyme'; | |||
import AllProjects from '../AllProjects'; | |||
import { getView, saveSort, saveView, saveVisualization } from '../../../../helpers/storage'; | |||
beforeEach(() => { | |||
(getView as jest.Mock<any>).mockImplementation(() => null); | |||
(saveSort as jest.Mock<any>).mockClear(); | |||
(saveView as jest.Mock<any>).mockClear(); | |||
(saveVisualization as jest.Mock<any>).mockClear(); | |||
}); | |||
it('renders', () => { | |||
const wrapper = shallow( | |||
<AllProjects | |||
fetchProjects={jest.fn()} | |||
isFavorite={false} | |||
location={{ pathname: '/projects', query: {} }} | |||
/>, | |||
{ context: { router: {} } } | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ query: { view: 'visualizations' } }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('fetches projects', () => { | |||
const fetchProjects = jest.fn(); | |||
mountRender({ fetchProjects }); | |||
expect(fetchProjects).lastCalledWith( | |||
{ | |||
coverage: null, | |||
duplications: null, | |||
gate: null, | |||
languages: null, | |||
maintainability: null, | |||
new_coverage: null, | |||
new_duplications: null, | |||
new_lines: null, | |||
new_maintainability: null, | |||
new_reliability: null, | |||
new_security: null, | |||
reliability: null, | |||
search: null, | |||
security: null, | |||
size: null, | |||
sort: null, | |||
tags: null, | |||
view: undefined, | |||
visualization: null | |||
}, | |||
false, | |||
undefined | |||
); | |||
}); | |||
it('redirects to the saved search', () => { | |||
(getView as jest.Mock<any>).mockImplementation(() => 'leak'); | |||
const replace = jest.fn(); | |||
mountRender({}, jest.fn(), replace); | |||
expect(replace).lastCalledWith({ pathname: '/projects', query: { view: 'leak' } }); | |||
}); | |||
it('changes sort', () => { | |||
const push = jest.fn(); | |||
const wrapper = mountRender({}, push); | |||
wrapper.find('PageHeaderContainer').prop<Function>('onSortChange')('size', false); | |||
expect(push).lastCalledWith({ pathname: '/projects', query: { sort: 'size' } }); | |||
expect(saveSort).lastCalledWith('size'); | |||
}); | |||
it('changes perspective to leak', () => { | |||
const push = jest.fn(); | |||
const wrapper = mountRender({}, push); | |||
wrapper.find('PageHeaderContainer').prop<Function>('onPerspectiveChange')({ view: 'leak' }); | |||
expect(push).lastCalledWith({ | |||
pathname: '/projects', | |||
query: { view: 'leak', visualization: undefined } | |||
}); | |||
expect(saveSort).lastCalledWith(undefined); | |||
expect(saveView).lastCalledWith('leak'); | |||
expect(saveVisualization).lastCalledWith(undefined); | |||
}); | |||
it('updates sorting when changing perspective from leak', () => { | |||
const push = jest.fn(); | |||
const wrapper = mountRender( | |||
{ location: { pathname: '/projects', query: { sort: 'new_coverage', view: 'leak' } } }, | |||
push | |||
); | |||
wrapper.find('PageHeaderContainer').prop<Function>('onPerspectiveChange')({ | |||
view: undefined | |||
}); | |||
expect(push).lastCalledWith({ | |||
pathname: '/projects', | |||
query: { sort: 'coverage', view: undefined, visualization: undefined } | |||
}); | |||
expect(saveSort).lastCalledWith('coverage'); | |||
expect(saveView).lastCalledWith(undefined); | |||
expect(saveVisualization).lastCalledWith(undefined); | |||
}); | |||
it('changes perspective to risk visualization', () => { | |||
const push = jest.fn(); | |||
const wrapper = mountRender({}, push); | |||
wrapper.find('PageHeaderContainer').prop<Function>('onPerspectiveChange')({ | |||
view: 'visualizations', | |||
visualization: 'risk' | |||
}); | |||
expect(push).lastCalledWith({ | |||
pathname: '/projects', | |||
query: { view: 'visualizations', visualization: 'risk' } | |||
}); | |||
expect(saveSort).lastCalledWith(undefined); | |||
expect(saveView).lastCalledWith('visualizations'); | |||
expect(saveVisualization).lastCalledWith('risk'); | |||
}); | |||
function mountRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) { | |||
return mount( | |||
<AllProjects | |||
fetchProjects={jest.fn()} | |||
isFavorite={false} | |||
location={{ pathname: '/projects', query: {} }} | |||
{...props} | |||
/>, | |||
{ context: { router: { push, replace } } } | |||
); | |||
} |
@@ -0,0 +1,89 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
jest.mock('../AllProjectsContainer', () => ({ | |||
default: function AllProjectsContainer() { | |||
return null; | |||
} | |||
})); | |||
jest.mock('../../../../helpers/storage', () => ({ | |||
isFavoriteSet: jest.fn(), | |||
isAllSet: jest.fn() | |||
})); | |||
jest.mock('../../../../api/components', () => ({ | |||
searchProjects: jest.fn() | |||
})); | |||
import * as React from 'react'; | |||
import { mount } from 'enzyme'; | |||
import { UnconnectedDefaultPageSelector } from '../DefaultPageSelector'; | |||
import { doAsync } from '../../../../helpers/testUtils'; | |||
const isFavoriteSet = require('../../../../helpers/storage').isFavoriteSet as jest.Mock<any>; | |||
const isAllSet = require('../../../../helpers/storage').isAllSet as jest.Mock<any>; | |||
const searchProjects = require('../../../../api/components').searchProjects as jest.Mock<any>; | |||
beforeEach(() => { | |||
isFavoriteSet.mockImplementation(() => false).mockClear(); | |||
isAllSet.mockImplementation(() => false).mockClear(); | |||
}); | |||
it('shows all projects with existing filter', () => { | |||
const replace = jest.fn(); | |||
mountRender(undefined, { size: '1' }, replace); | |||
expect(replace).not.toBeCalled(); | |||
}); | |||
it('shows all projects sorted by analysis date for anonymous', () => { | |||
const replace = jest.fn(); | |||
mountRender({ isLoggedIn: false }, undefined, replace); | |||
expect(replace).lastCalledWith({ query: { sort: '-analysis_date' } }); | |||
}); | |||
it('shows favorite projects', () => { | |||
isFavoriteSet.mockImplementation(() => true); | |||
const replace = jest.fn(); | |||
mountRender(undefined, undefined, replace); | |||
expect(replace).lastCalledWith({ pathname: '/projects/favorite', query: {} }); | |||
}); | |||
it('shows all projects', () => { | |||
isAllSet.mockImplementation(() => true); | |||
const replace = jest.fn(); | |||
mountRender(undefined, undefined, replace); | |||
expect(replace).not.toBeCalled(); | |||
}); | |||
it('fetches favorites', () => { | |||
searchProjects.mockImplementation(() => Promise.resolve({ paging: { total: 3 } })); | |||
const replace = jest.fn(); | |||
mountRender(undefined, undefined, replace); | |||
return doAsync().then(() => { | |||
expect(searchProjects).toHaveBeenLastCalledWith({ filter: 'isFavorite', ps: 1 }); | |||
expect(replace).toBeCalledWith({ pathname: '/projects/favorite', query: {} }); | |||
}); | |||
}); | |||
function mountRender(user: any = { isLoggedIn: true }, query: any = {}, replace: any = jest.fn()) { | |||
return mount(<UnconnectedDefaultPageSelector currentUser={user} location={{ query }} />, { | |||
context: { router: { replace } } | |||
}); | |||
} |
@@ -0,0 +1,26 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import EmptyInstance from '../EmptyInstance'; | |||
it('renders', () => { | |||
expect(shallow(<EmptyInstance />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,69 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
jest.mock('../../../../helpers/storage', () => ({ | |||
saveAll: jest.fn(), | |||
saveFavorite: jest.fn() | |||
})); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import FavoriteFilter from '../FavoriteFilter'; | |||
import { saveAll, saveFavorite } from '../../../../helpers/storage'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const user = { isLoggedIn: true }; | |||
const query = { size: 1 }; | |||
beforeEach(() => { | |||
(saveAll as jest.Mock<any>).mockClear(); | |||
(saveFavorite as jest.Mock<any>).mockClear(); | |||
}); | |||
it('renders for logged in user', () => { | |||
expect(shallow(<FavoriteFilter query={query} user={user} />)).toMatchSnapshot(); | |||
}); | |||
it('saves last selection', () => { | |||
const wrapper = shallow(<FavoriteFilter query={query} user={user} />); | |||
click(wrapper.find('#favorite-projects')); | |||
expect(saveFavorite).toBeCalled(); | |||
click(wrapper.find('#all-projects')); | |||
expect(saveAll).toBeCalled(); | |||
}); | |||
it('handles organization', () => { | |||
expect( | |||
shallow(<FavoriteFilter organization={{ key: 'org' }} query={query} user={user} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('does not save last selection with organization', () => { | |||
const wrapper = shallow( | |||
<FavoriteFilter organization={{ key: 'org' }} query={query} user={user} /> | |||
); | |||
click(wrapper.find('#favorite-projects')); | |||
expect(saveFavorite).not.toBeCalled(); | |||
click(wrapper.find('#all-projects')); | |||
expect(saveAll).not.toBeCalled(); | |||
}); | |||
it('does not render for anonymous', () => { | |||
expect(shallow(<FavoriteFilter query={query} user={{ isLoggedIn: false }} />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,26 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import NoFavoriteProjects from '../NoFavoriteProjects'; | |||
it('renders', () => { | |||
expect(shallow(<NoFavoriteProjects />)).toMatchSnapshot(); | |||
}); |
@@ -17,31 +17,21 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PageHeader from '../PageHeader'; | |||
it('should render correctly', () => { | |||
expect( | |||
shallow(<PageHeader query={{ search: 'test' }} projectsAppState={{ total: 12 }} />) | |||
).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render correctly while loading', () => { | |||
expect( | |||
shallow( | |||
<PageHeader | |||
query={{ search: '' }} | |||
isFavorite={true} | |||
projectsAppState={{ loading: true, total: 2 }} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect(shallowRender({ projectsAppState: { loading: true, total: 2 } })).toMatchSnapshot(); | |||
}); | |||
it('should not render projects total', () => { | |||
expect( | |||
shallow(<PageHeader projectsAppState={{}} query={{ search: '' }} />) | |||
shallowRender({ projectsAppState: {} }) | |||
.find('#projects-total') | |||
.exists() | |||
).toBeFalsy(); | |||
@@ -49,38 +39,47 @@ it('should not render projects total', () => { | |||
it('should render disabled sorting options for visualizations', () => { | |||
expect( | |||
shallow( | |||
<PageHeader | |||
open={true} | |||
projectsAppState={{}} | |||
view="visualizations" | |||
visualization="coverage" | |||
/> | |||
) | |||
shallowRender({ | |||
open: true, | |||
projectsAppState: {}, | |||
view: 'visualizations', | |||
visualization: 'coverage' | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render switch the default sorting option for anonymous users', () => { | |||
expect( | |||
shallow( | |||
<PageHeader | |||
currentUser={{ isLoggedIn: true }} | |||
open={true} | |||
projectsAppState={{}} | |||
view="overall" | |||
visualization="risk" | |||
/> | |||
).find('ProjectsSortingSelect') | |||
shallowRender({ | |||
currentUser: { isLoggedIn: true }, | |||
open: true, | |||
projectsAppState: {}, | |||
visualization: 'risk' | |||
}).find('ProjectsSortingSelect') | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<PageHeader | |||
currentUser={{ isLoggedIn: false }} | |||
open={true} | |||
projectsAppState={{}} | |||
view="leak" | |||
visualization="risk" | |||
/> | |||
).find('ProjectsSortingSelect') | |||
shallowRender({ | |||
currentUser: { isLoggedIn: false }, | |||
open: true, | |||
projectsAppState: {}, | |||
view: 'leak', | |||
visualization: 'risk' | |||
}).find('ProjectsSortingSelect') | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props?: any) { | |||
return shallow( | |||
<PageHeader | |||
onPerspectiveChange={jest.fn()} | |||
onSortChange={jest.fn()} | |||
projects={[]} | |||
projectsAppState={{ loading: false, total: 12 }} | |||
query={{ search: 'test' }} | |||
selectedSort="size" | |||
view="overall" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PageSidebar from '../PageSidebar'; | |||
@@ -17,17 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PerspectiveSelect from '../PerspectiveSelect'; | |||
it('should render correctly', () => { | |||
expect(shallow(<PerspectiveSelect view="overall" />)).toMatchSnapshot(); | |||
expect(shallow(<PerspectiveSelect onChange={jest.fn()} view="overall" />)).toMatchSnapshot(); | |||
}); | |||
it('should render with coverage selected', () => { | |||
expect( | |||
shallow(<PerspectiveSelect view="visualizations" visualization="coverage" />) | |||
shallow( | |||
<PerspectiveSelect onChange={jest.fn()} view="visualizations" visualization="coverage" /> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -35,9 +37,9 @@ it('should handle perspective change correctly', () => { | |||
const onChange = jest.fn(); | |||
const instance = shallow( | |||
<PerspectiveSelect view="visualizations" visualization="coverage" onChange={onChange} /> | |||
).instance(); | |||
instance.handleChange({ value: 'overall', type: 'view' }); | |||
instance.handleChange({ value: 'leak', type: 'view' }); | |||
instance.handleChange({ value: 'coverage', type: 'visualization' }); | |||
).instance() as PerspectiveSelect; | |||
instance.handleChange({ label: 'overall', value: 'overall', type: 'view' }); | |||
instance.handleChange({ label: 'leak', value: 'leak', type: 'view' }); | |||
instance.handleChange({ label: 'coverage', value: 'coverage', type: 'visualization' }); | |||
expect(onChange.mock.calls).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,80 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PerspectiveSelectOption from '../PerspectiveSelectOption'; | |||
it('should render correctly for a view', () => { | |||
expect( | |||
shallow( | |||
<PerspectiveSelectOption | |||
onFocus={jest.fn()} | |||
onSelect={jest.fn()} | |||
option={{ value: 'overall', type: 'view', label: 'Overall' }}> | |||
Overall | |||
</PerspectiveSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render correctly for a visualization', () => { | |||
expect( | |||
shallow( | |||
<PerspectiveSelectOption | |||
onFocus={jest.fn()} | |||
onSelect={jest.fn()} | |||
option={{ value: 'coverage', type: 'visualization', label: 'Coverage' }}> | |||
Coverage | |||
</PerspectiveSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('selects option', () => { | |||
const onSelect = jest.fn(); | |||
const option = { value: 'coverage', type: 'visualization', label: 'Coverage' }; | |||
const wrapper = shallow( | |||
<PerspectiveSelectOption onFocus={jest.fn()} onSelect={onSelect} option={option} /> | |||
); | |||
const event = { stopPropagation() {}, preventDefault() {} }; | |||
wrapper.simulate('mousedown', event); | |||
expect(onSelect).toBeCalledWith(option, event); | |||
}); | |||
it('focuses option', () => { | |||
const onFocus = jest.fn(); | |||
const option = { value: 'coverage', type: 'visualization', label: 'Coverage' }; | |||
const wrapper = shallow( | |||
<PerspectiveSelectOption onFocus={onFocus} onSelect={jest.fn()} option={option} /> | |||
); | |||
const event = { stopPropagation() {}, preventDefault() {} }; | |||
wrapper.simulate('mouseenter', event); | |||
expect(onFocus).toBeCalledWith(option, event); | |||
onFocus.mockClear(); | |||
wrapper.simulate('mousemove', event); | |||
expect(onFocus).toBeCalledWith(option, event); | |||
onFocus.mockClear(); | |||
wrapper.setProps({ isFocused: true }); | |||
wrapper.simulate('mousemove', event); | |||
expect(onFocus).not.toBeCalled(); | |||
}); |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardLanguages from '../ProjectCardLanguages'; | |||
const languages = { | |||
java: { key: 'java', name: 'Java' }, | |||
js: { key: 'js', name: 'JavaScript' } | |||
}; | |||
it('renders', () => { | |||
expect( | |||
shallow(<ProjectCardLanguages distribution="java=137;js=15" languages={languages} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('sorts languages', () => { | |||
expect( | |||
shallow(<ProjectCardLanguages distribution="java=13;js=152" languages={languages} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('handles unknown languages', () => { | |||
expect( | |||
shallow(<ProjectCardLanguages distribution="java=13;cpp=18" languages={languages} />) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow(<ProjectCardLanguages distribution="java=13;<null>=18" languages={languages} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('does not render', () => { | |||
expect(shallow(<ProjectCardLanguages languages={languages} />)).toMatchSnapshot(); | |||
}); |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardLeak from '../ProjectCardLeak'; | |||
@@ -32,11 +32,11 @@ const MEASURES = { | |||
alert_status: 'OK', | |||
reliability_rating: '1.0', | |||
sqale_rating: '1.0', | |||
new_bugs: 12 | |||
new_bugs: '12' | |||
}; | |||
it('should display analysis date and leak start date', () => { | |||
const card = shallow(<ProjectCardLeak type="leak" measures={MEASURES} project={PROJECT} />); | |||
const card = shallow(<ProjectCardLeak measures={MEASURES} project={PROJECT} />); | |||
expect(card.find('.project-card-dates').exists()).toBeTruthy(); | |||
expect(card.find('.project-card-dates').find('DateFromNow')).toHaveLength(1); | |||
expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1); | |||
@@ -44,14 +44,14 @@ it('should display analysis date and leak start date', () => { | |||
it('should not display analysis date or leak start date', () => { | |||
const project = { ...PROJECT, analysisDate: undefined }; | |||
const card = shallow(<ProjectCardLeak type="leak" measures={MEASURES} project={project} />); | |||
const card = shallow(<ProjectCardLeak measures={MEASURES} project={project} />); | |||
expect(card.find('.project-card-dates').exists()).toBeFalsy(); | |||
}); | |||
it('should display loading', () => { | |||
const measures = { ...MEASURES, new_bugs: undefined }; | |||
const measures = { alert_status: 'OK', reliability_rating: '1.0', sqale_rating: '1.0' }; | |||
expect( | |||
shallow(<ProjectCardLeak type="leak" measures={measures} project={PROJECT} />) | |||
shallow(<ProjectCardLeak measures={measures} project={PROJECT} />) | |||
.find('.boxed-group') | |||
.hasClass('boxed-group-loading') | |||
).toBeTruthy(); | |||
@@ -60,7 +60,7 @@ it('should display loading', () => { | |||
it('should display tags', () => { | |||
const project = { ...PROJECT, tags: ['foo', 'bar'] }; | |||
expect( | |||
shallow(<ProjectCardLeak type="leak" project={project} />) | |||
shallow(<ProjectCardLeak project={project} />) | |||
.find('TagsList') | |||
.exists() | |||
).toBeTruthy(); | |||
@@ -69,14 +69,12 @@ it('should display tags', () => { | |||
it('should private badge', () => { | |||
const project = { ...PROJECT, visibility: 'private' }; | |||
expect( | |||
shallow(<ProjectCardLeak type="leak" project={project} />) | |||
shallow(<ProjectCardLeak project={project} />) | |||
.find('PrivateBadge') | |||
.exists() | |||
).toBeTruthy(); | |||
}); | |||
it('should display the leak measures and quality gate', () => { | |||
expect( | |||
shallow(<ProjectCardLeak type="leak" measures={MEASURES} project={PROJECT} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<ProjectCardLeak measures={MEASURES} project={PROJECT} />)).toMatchSnapshot(); | |||
}); |
@@ -17,38 +17,37 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardLeakMeasures from '../ProjectCardLeakMeasures'; | |||
const measures = { | |||
alert_status: 'ERROR', | |||
new_reliability_rating: '1.0', | |||
new_bugs: '8', | |||
new_security_rating: '2.0', | |||
new_vulnerabilities: '2', | |||
new_maintainability_rating: '1.0', | |||
new_code_smells: '0', | |||
new_coverage: '26.55', | |||
new_duplicated_lines_density: '0.55', | |||
new_lines: '87' | |||
}; | |||
it('should render correctly with all data', () => { | |||
const measures = { | |||
alert_status: 'ERROR', | |||
new_reliability_rating: '1.0', | |||
new_bugs: '8', | |||
new_security_rating: '2.0', | |||
new_vulnerabilities: '2', | |||
new_maintainability_rating: '1.0', | |||
new_code_smells: '0', | |||
new_coverage: '26.55', | |||
new_duplicated_lines_density: '0.55', | |||
new_lines: '87' | |||
}; | |||
const wrapper = shallow(<ProjectCardLeakMeasures measures={measures} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render no data style new coverage, new duplications and new lines', () => { | |||
const wrapper = shallow( | |||
<ProjectCardLeakMeasures | |||
measures={{ | |||
...measures, | |||
new_coverage: undefined, | |||
new_duplicated_lines_density: undefined, | |||
new_lines: undefined | |||
}} | |||
/> | |||
); | |||
const measures = { | |||
alert_status: 'ERROR', | |||
new_reliability_rating: '1.0', | |||
new_bugs: '8', | |||
new_security_rating: '2.0', | |||
new_vulnerabilities: '2', | |||
new_maintainability_rating: '1.0', | |||
new_code_smells: '0' | |||
}; | |||
const wrapper = shallow(<ProjectCardLeakMeasures measures={measures} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -17,13 +17,12 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardOverall from '../ProjectCardOverall'; | |||
const PROJECT = { | |||
analysisDate: '2017-01-01', | |||
leakPeriodDate: '2016-12-01', | |||
key: 'foo', | |||
name: 'Foo', | |||
tags: [] | |||
@@ -32,7 +31,7 @@ const MEASURES = { | |||
alert_status: 'OK', | |||
reliability_rating: '1.0', | |||
sqale_rating: '1.0', | |||
new_bugs: 12 | |||
new_bugs: '12' | |||
}; | |||
it('should display analysis date (and not leak period) when defined', () => { | |||
@@ -49,14 +48,13 @@ it('should display analysis date (and not leak period) when defined', () => { | |||
}); | |||
it('should display loading', () => { | |||
const measures = { ...MEASURES, sqale_rating: undefined }; | |||
expect( | |||
shallow(<ProjectCardOverall project={PROJECT} />) | |||
.find('.boxed-group') | |||
.hasClass('boxed-group-loading') | |||
).toBeTruthy(); | |||
expect( | |||
shallow(<ProjectCardOverall measures={measures} project={PROJECT} />) | |||
shallow(<ProjectCardOverall measures={{ sqale_rating: '1.0' }} project={PROJECT} />) | |||
.find('.boxed-group') | |||
.hasClass('boxed-group-loading') | |||
).toBeTruthy(); | |||
@@ -83,7 +81,7 @@ it('should display tags', () => { | |||
it('should private badge', () => { | |||
const project = { ...PROJECT, visibility: 'private' }; | |||
expect( | |||
shallow(<ProjectCardOverall type="overall" project={project} />) | |||
shallow(<ProjectCardOverall project={project} />) | |||
.find('PrivateBadge') | |||
.exists() | |||
).toBeTruthy(); |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardOverallMeasures from '../ProjectCardOverallMeasures'; | |||
@@ -0,0 +1,30 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectCardQualityGate from '../ProjectCardQualityGate'; | |||
it('renders', () => { | |||
expect(shallow(<ProjectCardQualityGate status="ERROR" />)).toMatchSnapshot(); | |||
}); | |||
it('does not render', () => { | |||
expect(shallow(<ProjectCardQualityGate />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectsList from '../ProjectsList'; | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('does not render without projects', () => { | |||
expect(shallow(<ProjectsList isFavorite={false} isFiltered={false} />)).toMatchSnapshot(); | |||
}); | |||
it('renders different types of "no projects"', () => { | |||
expect(shallowRender({ projects: [] })).toMatchSnapshot(); | |||
expect(shallowRender({ projects: [], isFiltered: true })).toMatchSnapshot(); | |||
expect(shallowRender({ projects: [], isFavorite: true })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props?: any) { | |||
return shallow( | |||
<ProjectsList | |||
cardType="overall" | |||
isFavorite={false} | |||
isFiltered={false} | |||
projects={['foo', 'bar']} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,13 +17,21 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectsSortingSelect from '../ProjectsSortingSelect'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render correctly for overall view', () => { | |||
expect( | |||
shallow(<ProjectsSortingSelect selectedSort="name" view="overall" defaultOption="name" />) | |||
shallow( | |||
<ProjectsSortingSelect | |||
defaultOption="name" | |||
onChange={jest.fn()} | |||
selectedSort="name" | |||
view="overall" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -31,9 +39,10 @@ it('should render correctly for leak view', () => { | |||
expect( | |||
shallow( | |||
<ProjectsSortingSelect | |||
defaultOption="analysis_date" | |||
onChange={jest.fn()} | |||
selectedSort="new_coverage" | |||
view="leak" | |||
defaultOption="analysis_date" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
@@ -42,7 +51,40 @@ it('should render correctly for leak view', () => { | |||
it('should handle the descending sort direction', () => { | |||
expect( | |||
shallow( | |||
<ProjectsSortingSelect selectedSort="-vulnerability" view="overall" defaultOption="name" /> | |||
<ProjectsSortingSelect | |||
defaultOption="name" | |||
onChange={jest.fn()} | |||
selectedSort="-vulnerability" | |||
view="overall" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('changes sorting', () => { | |||
const onChange = jest.fn(); | |||
const instance = shallow( | |||
<ProjectsSortingSelect | |||
defaultOption="name" | |||
onChange={onChange} | |||
selectedSort="-vulnerabilities" | |||
view="overall" | |||
/> | |||
).instance() as ProjectsSortingSelect; | |||
instance.handleSortChange({ label: 'size', value: 'size' }); | |||
expect(onChange).toBeCalledWith('size', true); | |||
}); | |||
it('reverses sorting', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallow( | |||
<ProjectsSortingSelect | |||
defaultOption="name" | |||
onChange={onChange} | |||
selectedSort="-size" | |||
view="overall" | |||
/> | |||
); | |||
click(wrapper.find('a')); | |||
expect(onChange).toBeCalledWith('size', false); | |||
}); |
@@ -0,0 +1,80 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectsSortingSelectOption from '../ProjectsSortingSelectOption'; | |||
it('renders', () => { | |||
expect( | |||
shallow( | |||
<ProjectsSortingSelectOption | |||
onFocus={jest.fn()} | |||
onSelect={jest.fn()} | |||
option={{ label: 'Size', value: 'size' }}> | |||
Size | |||
</ProjectsSortingSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders short', () => { | |||
expect( | |||
shallow( | |||
<ProjectsSortingSelectOption | |||
onFocus={jest.fn()} | |||
onSelect={jest.fn()} | |||
option={{ label: 'Size', short: 'Short', value: 'size' }}> | |||
Size | |||
</ProjectsSortingSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('selects option', () => { | |||
const onSelect = jest.fn(); | |||
const option = { value: 'coverage', type: 'visualization', label: 'Coverage' }; | |||
const wrapper = shallow( | |||
<ProjectsSortingSelectOption onFocus={jest.fn()} onSelect={onSelect} option={option} /> | |||
); | |||
const event = { stopPropagation() {}, preventDefault() {} }; | |||
wrapper.simulate('mousedown', event); | |||
expect(onSelect).toBeCalledWith(option, event); | |||
}); | |||
it('focuses option', () => { | |||
const onFocus = jest.fn(); | |||
const option = { value: 'coverage', type: 'visualization', label: 'Coverage' }; | |||
const wrapper = shallow( | |||
<ProjectsSortingSelectOption onFocus={onFocus} onSelect={jest.fn()} option={option} /> | |||
); | |||
const event = { stopPropagation() {}, preventDefault() {} }; | |||
wrapper.simulate('mouseenter', event); | |||
expect(onFocus).toBeCalledWith(option, event); | |||
onFocus.mockClear(); | |||
wrapper.simulate('mousemove', event); | |||
expect(onFocus).toBeCalledWith(option, event); | |||
onFocus.mockClear(); | |||
wrapper.setProps({ isFocused: true }); | |||
wrapper.simulate('mousemove', event); | |||
expect(onFocus).not.toBeCalled(); | |||
}); |
@@ -0,0 +1,155 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="layout-page projects-page" | |||
> | |||
<HelmetWrapper | |||
encodeSpecialCharacters={true} | |||
title="projects.page" | |||
/> | |||
<div | |||
className="layout-page-side-outer" | |||
> | |||
<div | |||
className="layout-page-side projects-page-side" | |||
style={ | |||
Object { | |||
"top": 30, | |||
} | |||
} | |||
> | |||
<div | |||
className="layout-page-side-inner" | |||
> | |||
<div | |||
className="layout-page-filters" | |||
> | |||
<PageSidebar | |||
isFavorite={false} | |||
query={Object {}} | |||
view="overall" | |||
visualization="risk" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="layout-page-main projects-page-content" | |||
> | |||
<div | |||
className="layout-page-header-panel layout-page-main-header" | |||
> | |||
<div | |||
className="layout-page-header-panel-inner layout-page-main-header-inner" | |||
> | |||
<div | |||
className="layout-page-main-inner" | |||
> | |||
<PageHeaderContainer | |||
isFavorite={false} | |||
onPerspectiveChange={[Function]} | |||
onSortChange={[Function]} | |||
query={Object {}} | |||
selectedSort="name" | |||
view="overall" | |||
visualization="risk" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="layout-page-main-inner" | |||
> | |||
<ProjectsListContainer | |||
cardType="overall" | |||
isFavorite={false} | |||
isFiltered={false} | |||
/> | |||
<ProjectsListFooterContainer | |||
isFavorite={false} | |||
query={Object {}} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`renders 2`] = ` | |||
<div | |||
className="layout-page projects-page" | |||
> | |||
<HelmetWrapper | |||
encodeSpecialCharacters={true} | |||
title="projects.page" | |||
/> | |||
<div | |||
className="layout-page-side-outer" | |||
> | |||
<div | |||
className="layout-page-side projects-page-side" | |||
style={ | |||
Object { | |||
"top": 30, | |||
} | |||
} | |||
> | |||
<div | |||
className="layout-page-side-inner" | |||
> | |||
<div | |||
className="layout-page-filters" | |||
> | |||
<PageSidebar | |||
isFavorite={false} | |||
query={ | |||
Object { | |||
"view": "visualizations", | |||
} | |||
} | |||
view="visualizations" | |||
visualization="risk" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="layout-page-main projects-page-content" | |||
> | |||
<div | |||
className="layout-page-header-panel layout-page-main-header" | |||
> | |||
<div | |||
className="layout-page-header-panel-inner layout-page-main-header-inner" | |||
> | |||
<div | |||
className="layout-page-main-inner" | |||
> | |||
<PageHeaderContainer | |||
isFavorite={false} | |||
onPerspectiveChange={[Function]} | |||
onSortChange={[Function]} | |||
query={ | |||
Object { | |||
"view": "visualizations", | |||
} | |||
} | |||
selectedSort="name" | |||
view="visualizations" | |||
visualization="risk" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="layout-page-main-inner" | |||
> | |||
<Connect(Visualizations) | |||
visualization="risk" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="projects-empty-list" | |||
> | |||
<h3> | |||
projects.no_projects.empty_instance | |||
</h3> | |||
</div> | |||
`; |
@@ -0,0 +1,93 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`does not render for anonymous 1`] = `null`; | |||
exports[`handles organization 1`] = ` | |||
<header | |||
className="page-header text-center" | |||
> | |||
<div | |||
className="button-group" | |||
> | |||
<Link | |||
activeClassName="button-active" | |||
className="button" | |||
id="favorite-projects" | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/org/projects/favorite", | |||
"query": Object { | |||
"size": 1, | |||
}, | |||
} | |||
} | |||
> | |||
my_favorites | |||
</Link> | |||
<IndexLink | |||
activeClassName="button-active" | |||
className="button" | |||
id="all-projects" | |||
onClick={[Function]} | |||
to={ | |||
Object { | |||
"pathname": "/organizations/org/projects", | |||
"query": Object { | |||
"size": 1, | |||
}, | |||
} | |||
} | |||
> | |||
all | |||
</IndexLink> | |||
</div> | |||
</header> | |||
`; | |||
exports[`renders for logged in user 1`] = ` | |||
<header | |||
className="page-header text-center" | |||
> | |||
<div | |||
className="button-group" | |||
> | |||
<Link | |||
activeClassName="button-active" | |||
className="button" | |||
id="favorite-projects" | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/projects/favorite", | |||
"query": Object { | |||
"size": 1, | |||
}, | |||
} | |||
} | |||
> | |||
my_favorites | |||
</Link> | |||
<IndexLink | |||
activeClassName="button-active" | |||
className="button" | |||
id="all-projects" | |||
onClick={[Function]} | |||
to={ | |||
Object { | |||
"pathname": "/projects", | |||
"query": Object { | |||
"size": 1, | |||
}, | |||
} | |||
} | |||
> | |||
all | |||
</IndexLink> | |||
</div> | |||
</header> | |||
`; |
@@ -0,0 +1,28 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="projects-empty-list" | |||
> | |||
<h3> | |||
projects.no_favorite_projects | |||
</h3> | |||
<p | |||
className="big-spacer-top" | |||
> | |||
projects.no_favorite_projects.engagement | |||
</p> | |||
<p | |||
className="big-spacer-top" | |||
> | |||
<Link | |||
className="button" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/projects/all" | |||
> | |||
projects.explore_projects | |||
</Link> | |||
</p> | |||
</div> | |||
`; |
@@ -6,12 +6,17 @@ exports[`should render correctly 1`] = ` | |||
> | |||
<PerspectiveSelect | |||
className="projects-topbar-item js-projects-perspective-select" | |||
onChange={[Function]} | |||
view="overall" | |||
/> | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption="analysis_date" | |||
onChange={[Function]} | |||
selectedSort="size" | |||
view="overall" | |||
/> | |||
<withRouter(SearchFilterContainer) | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
query={ | |||
Object { | |||
@@ -41,17 +46,21 @@ exports[`should render correctly while loading 1`] = ` | |||
> | |||
<PerspectiveSelect | |||
className="projects-topbar-item js-projects-perspective-select" | |||
onChange={[Function]} | |||
view="overall" | |||
/> | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption="analysis_date" | |||
onChange={[Function]} | |||
selectedSort="size" | |||
view="overall" | |||
/> | |||
<withRouter(SearchFilterContainer) | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
isFavorite={true} | |||
query={ | |||
Object { | |||
"search": "", | |||
"search": "test", | |||
} | |||
} | |||
/> | |||
@@ -80,6 +89,7 @@ exports[`should render disabled sorting options for visualizations 1`] = ` | |||
> | |||
<PerspectiveSelect | |||
className="projects-topbar-item js-projects-perspective-select" | |||
onChange={[Function]} | |||
view="visualizations" | |||
visualization="coverage" | |||
/> | |||
@@ -93,12 +103,19 @@ exports[`should render disabled sorting options for visualizations 1`] = ` | |||
<ProjectsSortingSelect | |||
className="js-projects-sorting-select" | |||
defaultOption="analysis_date" | |||
onChange={[Function]} | |||
selectedSort="size" | |||
view="visualizations" | |||
/> | |||
</div> | |||
</Tooltip> | |||
<withRouter(SearchFilterContainer) | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
query={ | |||
Object { | |||
"search": "test", | |||
} | |||
} | |||
/> | |||
<div | |||
className="projects-topbar-item is-last" | |||
@@ -110,6 +127,8 @@ exports[`should render switch the default sorting option for anonymous users 1`] | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption="name" | |||
onChange={[Function]} | |||
selectedSort="size" | |||
view="overall" | |||
/> | |||
`; | |||
@@ -118,6 +137,8 @@ exports[`should render switch the default sorting option for anonymous users 2`] | |||
<ProjectsSortingSelect | |||
className="projects-topbar-item js-projects-sorting-select" | |||
defaultOption="analysis_date" | |||
onChange={[Function]} | |||
selectedSort="size" | |||
view="leak" | |||
/> | |||
`; |
@@ -91,14 +91,13 @@ exports[`should render \`leak\` view correctly 1`] = ` | |||
/> | |||
<NewLinesFilter | |||
isFavorite={false} | |||
property="new_lines" | |||
query={ | |||
Object { | |||
"view": "leak", | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(LanguagesFilter)) | |||
<Connect(LanguagesFilter) | |||
isFavorite={false} | |||
query={ | |||
Object { | |||
@@ -106,7 +105,7 @@ exports[`should render \`leak\` view correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(TagsFilter)) | |||
<Connect(TagsFilter) | |||
isFavorite={false} | |||
query={ | |||
Object { | |||
@@ -178,7 +177,6 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
<CoverageFilter | |||
isFavorite={true} | |||
property="coverage" | |||
query={ | |||
Object { | |||
"size": "3", | |||
@@ -187,7 +185,6 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
<DuplicationsFilter | |||
isFavorite={true} | |||
property="duplications" | |||
query={ | |||
Object { | |||
"size": "3", | |||
@@ -196,14 +193,13 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
<SizeFilter | |||
isFavorite={true} | |||
property="size" | |||
query={ | |||
Object { | |||
"size": "3", | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(LanguagesFilter)) | |||
<Connect(LanguagesFilter) | |||
isFavorite={true} | |||
query={ | |||
Object { | |||
@@ -211,7 +207,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
/> | |||
<Connect(withRouter(TagsFilter)) | |||
<Connect(TagsFilter) | |||
isFavorite={true} | |||
query={ | |||
Object { |
@@ -0,0 +1,107 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`does not render 1`] = `null`; | |||
exports[`handles unknown languages 1`] = ` | |||
<div | |||
className="project-card-languages" | |||
> | |||
<Tooltip | |||
overlay={ | |||
<span> | |||
<span> | |||
cpp | |||
<br /> | |||
</span> | |||
<span> | |||
Java | |||
<br /> | |||
</span> | |||
</span> | |||
} | |||
placement="bottom" | |||
> | |||
<span> | |||
cpp, Java | |||
</span> | |||
</Tooltip> | |||
</div> | |||
`; | |||
exports[`handles unknown languages 2`] = ` | |||
<div | |||
className="project-card-languages" | |||
> | |||
<Tooltip | |||
overlay={ | |||
<span> | |||
<span> | |||
unknown | |||
<br /> | |||
</span> | |||
<span> | |||
Java | |||
<br /> | |||
</span> | |||
</span> | |||
} | |||
placement="bottom" | |||
> | |||
<span> | |||
unknown, Java | |||
</span> | |||
</Tooltip> | |||
</div> | |||
`; | |||
exports[`renders 1`] = ` | |||
<div | |||
className="project-card-languages" | |||
> | |||
<Tooltip | |||
overlay={ | |||
<span> | |||
<span> | |||
Java | |||
<br /> | |||
</span> | |||
<span> | |||
JavaScript | |||
<br /> | |||
</span> | |||
</span> | |||
} | |||
placement="bottom" | |||
> | |||
<span> | |||
Java, JavaScript | |||
</span> | |||
</Tooltip> | |||
</div> | |||
`; | |||
exports[`sorts languages 1`] = ` | |||
<div | |||
className="project-card-languages" | |||
> | |||
<Tooltip | |||
overlay={ | |||
<span> | |||
<span> | |||
JavaScript | |||
<br /> | |||
</span> | |||
<span> | |||
Java | |||
<br /> | |||
</span> | |||
</span> | |||
} | |||
placement="bottom" | |||
> | |||
<span> | |||
JavaScript, Java | |||
</span> | |||
</Tooltip> | |||
</div> | |||
`; |
@@ -50,7 +50,7 @@ exports[`should display the leak measures and quality gate 1`] = ` | |||
measures={ | |||
Object { | |||
"alert_status": "OK", | |||
"new_bugs": 12, | |||
"new_bugs": "12", | |||
"reliability_rating": "1.0", | |||
"sqale_rating": "1.0", | |||
} |
@@ -28,8 +28,6 @@ exports[`should render correctly with all data 1`] = ` | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -67,8 +65,6 @@ exports[`should render correctly with all data 1`] = ` | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="2.0" | |||
/> | |||
</div> | |||
@@ -106,8 +102,6 @@ exports[`should render correctly with all data 1`] = ` | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -242,8 +236,6 @@ exports[`should render no data style new coverage, new duplications and new line | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -281,8 +273,6 @@ exports[`should render no data style new coverage, new duplications and new line | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="2.0" | |||
/> | |||
</div> | |||
@@ -320,8 +310,6 @@ exports[`should render no data style new coverage, new duplications and new line | |||
} | |||
/> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> |
@@ -47,7 +47,7 @@ exports[`should display the overall measures and quality gate 1`] = ` | |||
measures={ | |||
Object { | |||
"alert_status": "OK", | |||
"new_bugs": 12, | |||
"new_bugs": "12", | |||
"reliability_rating": "1.0", | |||
"sqale_rating": "1.0", | |||
} |
@@ -81,8 +81,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="project-card-measure-number" | |||
> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -104,8 +102,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="project-card-measure-number" | |||
> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -127,8 +123,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="project-card-measure-number" | |||
> | |||
<Rating | |||
muted={false} | |||
small={false} | |||
value="1.0" | |||
/> | |||
</div> | |||
@@ -153,8 +147,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="spacer-right" | |||
> | |||
<CoverageRating | |||
muted={false} | |||
size="normal" | |||
value="88.3" | |||
/> | |||
</span> | |||
@@ -192,8 +184,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="spacer-right" | |||
> | |||
<DuplicationsRating | |||
muted={false} | |||
small={false} | |||
value={9.8} | |||
/> | |||
</span> | |||
@@ -231,8 +221,6 @@ exports[`should render correctly with all data 1`] = ` | |||
className="spacer-right" | |||
> | |||
<SizeRating | |||
muted={false} | |||
small={false} | |||
value={2053} | |||
/> | |||
</span> | |||
@@ -274,8 +262,6 @@ exports[`should render ncloc correctly 1`] = ` | |||
className="spacer-right" | |||
> | |||
<SizeRating | |||
muted={false} | |||
small={false} | |||
value={16549887} | |||
/> | |||
</span> |
@@ -0,0 +1,23 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`does not render 1`] = `null`; | |||
exports[`renders 1`] = ` | |||
<div | |||
className="project-card-measure project-card-quality-gate spacer-left" | |||
> | |||
<Tooltip | |||
overlay="overview.quality_gate_x.ERROR" | |||
placement="bottom" | |||
> | |||
<div | |||
className="project-card-measure-inner" | |||
> | |||
<Level | |||
level="ERROR" | |||
small={true} | |||
/> | |||
</div> | |||
</Tooltip> | |||
</div> | |||
`; |
@@ -0,0 +1,42 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`does not render without projects 1`] = `null`; | |||
exports[`renders 1`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<Connect(ProjectCard) | |||
projectKey="foo" | |||
type="overall" | |||
/> | |||
<Connect(ProjectCard) | |||
projectKey="bar" | |||
type="overall" | |||
/> | |||
</div> | |||
`; | |||
exports[`renders different types of "no projects" 1`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<EmptyInstance /> | |||
</div> | |||
`; | |||
exports[`renders different types of "no projects" 2`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<EmptySearch /> | |||
</div> | |||
`; | |||
exports[`renders different types of "no projects" 3`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<NoFavoriteProjects /> | |||
</div> | |||
`; |
@@ -0,0 +1,25 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="" | |||
onMouseDown={[Function]} | |||
onMouseEnter={[Function]} | |||
onMouseMove={[Function]} | |||
title="Size" | |||
> | |||
Size | |||
</div> | |||
`; | |||
exports[`renders short 1`] = ` | |||
<div | |||
className="" | |||
onMouseDown={[Function]} | |||
onMouseEnter={[Function]} | |||
onMouseMove={[Function]} | |||
title="Size" | |||
> | |||
Short | |||
</div> | |||
`; |
@@ -1,84 +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 PropTypes from 'prop-types'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class CoverageFilter extends React.PureComponent { | |||
static propTypes = { | |||
className: PropTypes.string, | |||
query: PropTypes.object.isRequired, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
property: PropTypes.string | |||
}; | |||
static defaultProps = { | |||
property: 'coverage' | |||
}; | |||
getFacetValueForOption(facet, option) { | |||
const map = ['80.0-*', '70.0-80.0', '50.0-70.0', '30.0-50.0', '*-30.0', 'NO_DATA']; | |||
return facet[map[option - 1]]; | |||
} | |||
renderOption(option, selected) { | |||
return ( | |||
<span> | |||
{option < 6 && ( | |||
<CoverageRating | |||
value={getCoverageRatingAverageValue(option)} | |||
size="small" | |||
muted={!selected} | |||
/> | |||
)} | |||
<span className="spacer-left"> | |||
{option < 6 ? ( | |||
getCoverageRatingLabel(option) | |||
) : ( | |||
<span className="big-spacer-left">{translate('no_data')}</span> | |||
)} | |||
</span> | |||
</span> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property={this.props.property} | |||
className={this.props.className} | |||
options={[1, 2, 3, 4, 5, 6]} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
highlightUnder={1} | |||
highlightUnderMax={5} | |||
header={<FilterHeader name={translate('metric_domain.Coverage')} />} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import { Facet } from './Filter'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
} | |||
export default function CoverageFilter(props: Props) { | |||
const { property = 'coverage' } = props; | |||
return ( | |||
<FilterContainer | |||
property={property} | |||
className={props.className} | |||
options={[1, 2, 3, 4, 5, 6]} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
highlightUnder={1} | |||
highlightUnderMax={5} | |||
header={<FilterHeader name={translate('metric_domain.Coverage')} />} | |||
/> | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number): number { | |||
const map = ['80.0-*', '70.0-80.0', '50.0-70.0', '30.0-50.0', '*-30.0', 'NO_DATA']; | |||
return facet[map[option - 1]]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> | |||
{option < 6 && ( | |||
<CoverageRating | |||
value={getCoverageRatingAverageValue(option)} | |||
size="small" | |||
muted={!selected} | |||
/> | |||
)} | |||
<span className="spacer-left"> | |||
{option < 6 ? ( | |||
getCoverageRatingLabel(option) | |||
) : ( | |||
<span className="big-spacer-left">{translate('no_data')}</span> | |||
)} | |||
</span> | |||
</span> | |||
); | |||
} |
@@ -1,87 +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 PropTypes from 'prop-types'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import DuplicationsRating from '../../../components/ui/DuplicationsRating'; | |||
import { | |||
getDuplicationsRatingLabel, | |||
getDuplicationsRatingAverageValue | |||
} from '../../../helpers/ratings'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class DuplicationsFilter extends React.PureComponent { | |||
static propTypes = { | |||
className: PropTypes.string, | |||
query: PropTypes.object.isRequired, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
property: PropTypes.string | |||
}; | |||
static defaultProps = { | |||
property: 'duplications' | |||
}; | |||
getFacetValueForOption(facet, option) { | |||
const map = ['*-3.0', '3.0-5.0', '5.0-10.0', '10.0-20.0', '20.0-*', 'NO_DATA']; | |||
return facet[map[option - 1]]; | |||
} | |||
renderOption(option, selected) { | |||
return ( | |||
<span> | |||
{option < 6 && ( | |||
<DuplicationsRating | |||
value={getDuplicationsRatingAverageValue(option)} | |||
size="small" | |||
muted={!selected} | |||
/> | |||
)} | |||
<span className="spacer-left"> | |||
{option < 6 ? ( | |||
getDuplicationsRatingLabel(option) | |||
) : ( | |||
<span className="big-spacer-left">{translate('no_data')}</span> | |||
)} | |||
</span> | |||
</span> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property={this.props.property} | |||
className={this.props.className} | |||
options={[1, 2, 3, 4, 5, 6]} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
highlightUnder={1} | |||
highlightUnderMax={5} | |||
header={<FilterHeader name={translate('metric_domain.Duplications')} />} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,82 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import DuplicationsRating from '../../../components/ui/DuplicationsRating'; | |||
import { | |||
getDuplicationsRatingLabel, | |||
getDuplicationsRatingAverageValue | |||
} from '../../../helpers/ratings'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Facet } from './Filter'; | |||
export interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
} | |||
export default function DuplicationsFilter(props: Props) { | |||
const { property = 'duplications' } = props; | |||
return ( | |||
<FilterContainer | |||
property={property} | |||
className={props.className} | |||
options={[1, 2, 3, 4, 5, 6]} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
highlightUnder={1} | |||
highlightUnderMax={5} | |||
header={<FilterHeader name={translate('metric_domain.Duplications')} />} | |||
/> | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number) { | |||
const map = ['*-3.0', '3.0-5.0', '5.0-10.0', '10.0-20.0', '20.0-*', 'NO_DATA']; | |||
return facet[map[option - 1]]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> | |||
{option < 6 && ( | |||
<DuplicationsRating | |||
value={getDuplicationsRatingAverageValue(option)} | |||
size="small" | |||
muted={!selected} | |||
/> | |||
)} | |||
<span className="spacer-left"> | |||
{option < 6 ? ( | |||
getDuplicationsRatingLabel(option) | |||
) : ( | |||
<span className="big-spacer-left">{translate('no_data')}</span> | |||
)} | |||
</span> | |||
</span> | |||
); | |||
} |
@@ -17,60 +17,58 @@ | |||
* 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 PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Link } from 'react-router'; | |||
import { getFilterUrl } from './utils'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class Filter extends React.PureComponent { | |||
static propTypes = { | |||
property: PropTypes.string.isRequired, | |||
className: PropTypes.string, | |||
options: PropTypes.array.isRequired, | |||
query: PropTypes.object.isRequired, | |||
renderOption: PropTypes.func.isRequired, | |||
value: PropTypes.any, | |||
facet: PropTypes.object, | |||
maxFacetValue: PropTypes.number, | |||
optionClassName: PropTypes.string, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
getFacetValueForOption: PropTypes.func, | |||
halfWidth: PropTypes.bool, | |||
highlightUnder: PropTypes.number, | |||
highlightUnderMax: PropTypes.number, | |||
header: PropTypes.object, | |||
footer: PropTypes.object | |||
}; | |||
export type Option = string | number; | |||
export type Facet = { [x: string]: number }; | |||
static defaultProps = { | |||
halfWidth: false | |||
}; | |||
interface Props { | |||
property: string; | |||
className?: string; | |||
options: Option[]; | |||
query: { [x: string]: any }; | |||
renderOption: (option: Option, isSelected: boolean) => React.ReactNode; | |||
value?: Option | Option[]; | |||
facet?: Facet; | |||
maxFacetValue?: number; | |||
optionClassName?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
getFacetValueForOption?: (facet: Facet, option: Option) => void; | |||
halfWidth?: boolean; | |||
highlightUnder?: number; | |||
highlightUnderMax?: number; | |||
isSelected(option) { | |||
header?: React.ReactNode; | |||
footer?: React.ReactNode; | |||
} | |||
export default class Filter extends React.PureComponent<Props> { | |||
isSelected(option: Option): boolean { | |||
const { value } = this.props; | |||
return Array.isArray(value) ? value.includes(option) : option === value; | |||
} | |||
highlightUnder(option) { | |||
highlightUnder(option?: Option): boolean { | |||
return ( | |||
this.props.highlightUnder != null && | |||
option !== null && | |||
option != null && | |||
option > this.props.highlightUnder && | |||
(this.props.highlightUnderMax == null || option < this.props.highlightUnderMax) | |||
); | |||
} | |||
blurOnClick = (evt /*: Event & { currentTarget: HTMLElement } */) => evt.currentTarget.blur(); | |||
blurOnClick = (event: React.SyntheticEvent<HTMLElement>) => event.currentTarget.blur(); | |||
getPath(option) { | |||
getPath(option: Option) { | |||
const { property, value } = this.props; | |||
let urlOption; | |||
@@ -86,8 +84,8 @@ export default class Filter extends React.PureComponent { | |||
return getFilterUrl(this.props, { [property]: urlOption }); | |||
} | |||
renderOptionBar(facetValue) { | |||
if (facetValue == null || !this.props.maxFacetValue) { | |||
renderOptionBar(facetValue: number | undefined) { | |||
if (facetValue == undefined || !this.props.maxFacetValue) { | |||
return null; | |||
} | |||
return ( | |||
@@ -100,7 +98,7 @@ export default class Filter extends React.PureComponent { | |||
); | |||
} | |||
renderOption(option) { | |||
renderOption(option: Option) { | |||
const { facet, getFacetValueForOption, value } = this.props; | |||
const className = classNames( | |||
'facet', | |||
@@ -115,9 +113,13 @@ export default class Filter extends React.PureComponent { | |||
const path = this.getPath(option); | |||
const facetValue = | |||
facet && getFacetValueForOption ? getFacetValueForOption(facet, option) : null; | |||
facet && getFacetValueForOption ? getFacetValueForOption(facet, option) : undefined; | |||
const isUnderSelectedOption = this.highlightUnder(value) && option > value; | |||
const isUnderSelectedOption = | |||
typeof value === 'number' && | |||
typeof option === 'number' && | |||
this.highlightUnder(value) && | |||
option > value; | |||
return ( | |||
<Link | |||
@@ -139,7 +141,7 @@ export default class Filter extends React.PureComponent { | |||
); | |||
} | |||
renderOptions() { | |||
renderOptions = () => { | |||
const { options, highlightUnder } = this.props; | |||
if (options && options.length > 0) { | |||
if (highlightUnder != null) { | |||
@@ -166,7 +168,7 @@ export default class Filter extends React.PureComponent { | |||
} else { | |||
return <div className="search-navigator-facet-empty">{translate('no_results')}</div>; | |||
} | |||
} | |||
}; | |||
render() { | |||
return ( |
@@ -24,9 +24,10 @@ import { | |||
getProjectsAppMaxFacetValue | |||
} from '../../../store/rootReducer'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
const mapStateToProps = (state: any, ownProps: any) => ({ | |||
value: ownProps.query[ownProps.property], | |||
facet: getProjectsAppFacetByProperty(state, ownProps.property), | |||
maxFacetValue: getProjectsAppMaxFacetValue(state) | |||
}); | |||
export default connect(mapStateToProps)(Filter); | |||
export default connect<any, any, any>(mapStateToProps)(Filter); |
@@ -0,0 +1,34 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
interface Props { | |||
children?: React.ReactNode; | |||
name: string; | |||
} | |||
export default function FilterHeader(props: Props) { | |||
return ( | |||
<div className="search-navigator-facet-header projects-facet-header"> | |||
{props.name} | |||
{props.children} | |||
</div> | |||
); | |||
} |
@@ -1,72 +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 PropTypes from 'prop-types'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class IssuesFilter extends React.PureComponent { | |||
static propTypes = { | |||
className: PropTypes.string, | |||
headerDetail: PropTypes.element, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
name: PropTypes.string.isRequired, | |||
property: PropTypes.string.isRequired, | |||
query: PropTypes.object.isRequired | |||
}; | |||
getFacetValueForOption(facet, option) { | |||
return facet[option]; | |||
} | |||
renderOption(option, selected) { | |||
return ( | |||
<span> | |||
<Rating value={option} small={true} muted={!selected} /> | |||
{option > 1 && | |||
option < 5 && <span className="note spacer-left">{translate('and_worse')}</span>} | |||
</span> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property={this.props.property} | |||
className={this.props.className} | |||
options={[1, 2, 3, 4, 5]} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
highlightUnder={1} | |||
header={ | |||
<FilterHeader name={translate('metric_domain', this.props.name)}> | |||
{this.props.headerDetail} | |||
</FilterHeader> | |||
} | |||
/> | |||
); | |||
} | |||
} |
@@ -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. | |||
*/ | |||
import * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Facet } from './Filter'; | |||
interface Props { | |||
className?: string; | |||
headerDetail?: React.ReactNode; | |||
isFavorite?: boolean; | |||
name: string; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
} | |||
export default function IssuesFilter(props: Props) { | |||
return ( | |||
<FilterContainer | |||
property={props.property} | |||
className={props.className} | |||
options={[1, 2, 3, 4, 5]} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
highlightUnder={1} | |||
header={ | |||
<FilterHeader name={translate('metric_domain', props.name)}> | |||
{props.headerDetail} | |||
</FilterHeader> | |||
} | |||
/> | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number) { | |||
return facet[option]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> | |||
<Rating value={option} small={true} muted={!selected} /> | |||
{option > 1 && | |||
option < 5 && <span className="note spacer-left">{translate('and_worse')}</span>} | |||
</span> | |||
); | |||
} |
@@ -17,55 +17,49 @@ | |||
* 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 * as React from 'react'; | |||
import { difference, sortBy } from 'lodash'; | |||
import Filter from './Filter'; | |||
import Filter, { Facet } from './Filter'; | |||
import FilterHeader from './FilterHeader'; | |||
import SearchableFilterFooter from './SearchableFilterFooter'; | |||
import SearchableFilterOption from './SearchableFilterOption'; | |||
import { getLanguageByKey } from '../../../store/languages/reducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
query: {}, | |||
languages: {}, | |||
router: { push: ({ pathname: string, query?: {} }) => void }, | |||
value?: Array<string>, | |||
facet?: {}, | |||
isFavorite?: boolean, | |||
organization?: {}, | |||
maxFacetValue?: number | |||
}; | |||
*/ | |||
interface Languages { | |||
[key: string]: { key: string; name: string }; | |||
} | |||
const LIST_SIZE = 10; | |||
interface Props { | |||
facet?: Facet; | |||
isFavorite?: boolean; | |||
languages: Languages; | |||
maxFacetValue?: number; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
value?: Array<string>; | |||
} | |||
export default class LanguagesFilter extends React.PureComponent { | |||
/*:: props: Props; */ | |||
property = 'languages'; | |||
const LIST_SIZE = 10; | |||
getSearchOptions( | |||
facet /*: ?{} */, | |||
languages /*: {} */ | |||
) /*: Array<{ label: string, value: string }> */ { | |||
let languageKeys = Object.keys(languages); | |||
if (facet) { | |||
languageKeys = difference(languageKeys, Object.keys(facet)); | |||
export default class LanguagesFilter extends React.Component<Props> { | |||
getSearchOptions = () => { | |||
let languageKeys = Object.keys(this.props.languages); | |||
if (this.props.facet) { | |||
languageKeys = difference(languageKeys, Object.keys(this.props.facet)); | |||
} | |||
return languageKeys | |||
.slice(0, LIST_SIZE) | |||
.map(key => ({ label: languages[key].name, value: key })); | |||
} | |||
.map(key => ({ label: this.props.languages[key].name, value: key })); | |||
}; | |||
getSortedOptions(facet /*: {} */ = {}) { | |||
return sortBy(Object.keys(facet), [option => -facet[option], option => option]); | |||
} | |||
getSortedOptions = (facet: Facet = {}) => | |||
sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]); | |||
getFacetValueForOption = (facet /*: {} */ = {}, option /*: string */) => facet[option]; | |||
getFacetValueForOption = (facet: Facet = {}, option: string) => facet[option]; | |||
renderOption = (option /*: string */) => ( | |||
renderOption = (option: string) => ( | |||
<SearchableFilterOption | |||
optionKey={option} | |||
option={getLanguageByKey(this.props.languages, option)} | |||
@@ -73,9 +67,11 @@ export default class LanguagesFilter extends React.PureComponent { | |||
); | |||
render() { | |||
const { property = 'languages' } = this.props; | |||
return ( | |||
<Filter | |||
property={this.property} | |||
property={property} | |||
options={this.getSortedOptions(this.props.facet)} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
@@ -88,12 +84,11 @@ export default class LanguagesFilter extends React.PureComponent { | |||
header={<FilterHeader name={translate('projects.facets.languages')} />} | |||
footer={ | |||
<SearchableFilterFooter | |||
property={this.property} | |||
query={this.props.query} | |||
options={this.getSearchOptions(this.props.facet, this.props.languages)} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
router={this.props.router} | |||
options={this.getSearchOptions()} | |||
property={property} | |||
query={this.props.query} | |||
/> | |||
} | |||
/> |
@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import LanguagesFilter from './LanguagesFilter'; | |||
import { | |||
getProjectsAppFacetByProperty, | |||
@@ -26,10 +25,10 @@ import { | |||
getLanguages | |||
} from '../../../store/rootReducer'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
const mapStateToProps = (state: any, ownProps: any) => ({ | |||
languages: getLanguages(state), | |||
value: ownProps.query['languages'], | |||
facet: getProjectsAppFacetByProperty(state, 'languages'), | |||
maxFacetValue: getProjectsAppMaxFacetValue(state) | |||
}); | |||
export default connect(mapStateToProps)(withRouter(LanguagesFilter)); | |||
export default connect<any, any, any>(mapStateToProps)(LanguagesFilter); |
@@ -17,9 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import IssuesFilter from './IssuesFilter'; | |||
export default function MaintainabilityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
headerDetail?: React.ReactNode; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function MaintainabilityFilter(props: Props) { | |||
return <IssuesFilter {...props} name="Maintainability" property="maintainability" />; | |||
} |
@@ -17,9 +17,9 @@ | |||
* 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 CoverageFilter from './CoverageFilter'; | |||
import * as React from 'react'; | |||
import CoverageFilter, { Props } from './CoverageFilter'; | |||
export default function NewCoverageFilter(props) { | |||
export default function NewCoverageFilter(props: Props) { | |||
return <CoverageFilter {...props} property="new_coverage" className="leak-facet-box" />; | |||
} |
@@ -17,9 +17,9 @@ | |||
* 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 DuplicationsFilter from './DuplicationsFilter'; | |||
import * as React from 'react'; | |||
import DuplicationsFilter, { Props } from './DuplicationsFilter'; | |||
export default function NewDuplicationsFilter(props) { | |||
export default function NewDuplicationsFilter(props: Props) { | |||
return <DuplicationsFilter {...props} property="new_duplications" className="leak-facet-box" />; | |||
} |
@@ -1,71 +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 PropTypes from 'prop-types'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSizeRatingLabel } from '../../../helpers/ratings'; | |||
export default class NewLinesFilter extends React.PureComponent { | |||
static propTypes = { | |||
className: PropTypes.string, | |||
query: PropTypes.object.isRequired, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
property: PropTypes.string | |||
}; | |||
static defaultProps = { | |||
property: 'new_lines' | |||
}; | |||
getFacetValueForOption(facet, option) { | |||
const map = [ | |||
'*-1000.0', | |||
'1000.0-10000.0', | |||
'10000.0-100000.0', | |||
'100000.0-500000.0', | |||
'500000.0-*' | |||
]; | |||
return facet[map[option - 1]]; | |||
} | |||
renderOption(option) { | |||
return <span>{getSizeRatingLabel(option)}</span>; | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property={this.props.property} | |||
className="leak-facet-box" | |||
options={[1, 2, 3, 4, 5]} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
highlightUnder={1} | |||
header={<FilterHeader name={translate('projects.facets.new_lines')} />} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,61 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSizeRatingLabel } from '../../../helpers/ratings'; | |||
import { Facet } from './Filter'; | |||
export interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
} | |||
export default function NewLinesFilter(props: Props) { | |||
const { property = 'new_lines' } = props; | |||
return ( | |||
<FilterContainer | |||
property={property} | |||
className="leak-facet-box" | |||
options={[1, 2, 3, 4, 5]} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
highlightUnder={1} | |||
header={<FilterHeader name={translate('projects.facets.new_lines')} />} | |||
/> | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number) { | |||
const map = ['*-1000.0', '1000.0-10000.0', '10000.0-100000.0', '100000.0-500000.0', '500000.0-*']; | |||
return facet[map[option - 1]]; | |||
} | |||
function renderOption(option: number) { | |||
return <span>{getSizeRatingLabel(option)}</span>; | |||
} |
@@ -17,12 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon'; | |||
import IssuesFilter from './IssuesFilter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function NewMaintainabilityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function NewMaintainabilityFilter(props: Props) { | |||
return ( | |||
<IssuesFilter | |||
{...props} |
@@ -17,12 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import BugIcon from '../../../components/icons-components/BugIcon'; | |||
import IssuesFilter from './IssuesFilter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function NewReliabilityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function NewReliabilityFilter(props: Props) { | |||
return ( | |||
<IssuesFilter | |||
{...props} |
@@ -17,12 +17,19 @@ | |||
* 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 * as React from 'react'; | |||
import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon'; | |||
import IssuesFilter from './IssuesFilter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function NewSecurityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function NewSecurityFilter(props: Props) { | |||
return ( | |||
<IssuesFilter | |||
{...props} |
@@ -17,40 +17,39 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import Level from '../../../components/ui/Level'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Facet } from './Filter'; | |||
export default class QualityGateFilter extends React.PureComponent { | |||
static propTypes = { | |||
query: PropTypes.object.isRequired, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object | |||
}; | |||
export interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
getFacetValueForOption(facet, option) { | |||
return facet[option]; | |||
} | |||
export default function QualityGateFilter(props: Props) { | |||
return ( | |||
<FilterContainer | |||
property="gate" | |||
options={['OK', 'WARN', 'ERROR']} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
header={<FilterHeader name={translate('projects.facets.quality_gate')} />} | |||
/> | |||
); | |||
} | |||
renderOption(option, selected) { | |||
return <Level level={option} small={true} muted={!selected} />; | |||
} | |||
function getFacetValueForOption(facet: Facet, option: string) { | |||
return facet[option]; | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property="gate" | |||
options={['OK', 'WARN', 'ERROR']} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
header={<FilterHeader name={translate('projects.facets.quality_gate')} />} | |||
/> | |||
); | |||
} | |||
function renderOption(option: string, selected: boolean) { | |||
return <Level level={option} small={true} muted={!selected} />; | |||
} |
@@ -17,9 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import IssuesFilter from './IssuesFilter'; | |||
export default function ReliabilityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
headerDetail?: React.ReactNode; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function ReliabilityFilter(props: Props) { | |||
return <IssuesFilter {...props} name="Reliability" property="reliability" />; | |||
} |
@@ -17,50 +17,39 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
className?: string, | |||
handleSearch: (userString?: string) => void, | |||
query: { search?: string } | |||
}; | |||
*/ | |||
/*:: | |||
type State = { | |||
userQuery?: string | |||
}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
handleSearch: (userString?: string) => void; | |||
query: { search?: string | undefined }; | |||
} | |||
export default class SearchFilter extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
interface State { | |||
userQuery?: string; | |||
} | |||
constructor(props /*: Props */) { | |||
export default class SearchFilter extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
userQuery: props.query.search | |||
}; | |||
this.state = { userQuery: props.query.search }; | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
if ( | |||
this.props.query.search === this.state.userQuery && | |||
nextProps.query.search !== this.props.query.search | |||
) { | |||
this.setState({ | |||
userQuery: nextProps.query.search || '' | |||
}); | |||
this.setState({ userQuery: nextProps.query.search || '' }); | |||
} | |||
} | |||
handleQueryChange = ({ target } /*: { target: HTMLInputElement } */) => { | |||
this.setState({ userQuery: target.value }); | |||
if (!target.value || target.value.length >= 2) { | |||
this.props.handleSearch(target.value); | |||
handleQueryChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
const { value } = event.currentTarget; | |||
this.setState({ userQuery: value }); | |||
if (!value || value.length >= 2) { | |||
this.props.handleSearch(value); | |||
} | |||
}; | |||
@@ -17,36 +17,33 @@ | |||
* 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 { withRouter } from 'react-router'; | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { debounce } from 'lodash'; | |||
import { getFilterUrl } from './utils'; | |||
import SearchFilter from './SearchFilter'; | |||
/*:: | |||
type Props = {| | |||
className?: string, | |||
query: { search?: string }, | |||
router: { push: ({ pathname: string }) => void }, | |||
isFavorite?: boolean, | |||
organization?: {} | |||
|}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
query: { search?: string }; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
} | |||
class SearchFilterContainer extends React.PureComponent { | |||
/*:: handleSearch: (userQuery?: string) => void; */ | |||
/*:: props: Props; */ | |||
export default class SearchFilterContainer extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
constructor(props /*: Props */) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.handleSearch = debounce(this.handleSearch.bind(this), 250); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
handleSearch(userQuery /*: ?string */) { | |||
const path = getFilterUrl(this.props, { search: userQuery || null }); | |||
this.props.router.push(path); | |||
} | |||
handleSearch = (userQuery?: string) => { | |||
const path = getFilterUrl(this.props, { search: userQuery }); | |||
this.context.router.push(path); | |||
}; | |||
render() { | |||
return ( | |||
@@ -58,5 +55,3 @@ class SearchFilterContainer extends React.PureComponent { | |||
); | |||
} | |||
} | |||
export default withRouter(SearchFilterContainer); |
@@ -17,48 +17,47 @@ | |||
* 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 * as React from 'react'; | |||
import * as Select from 'react-select'; | |||
import * as PropTypes from 'prop-types'; | |||
import { getFilterUrl } from './utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
property: string, | |||
query: {}, | |||
options: Array<{ label: string, value: string }>, | |||
router: { push: ({ pathname: string, query?: {} }) => void }, | |||
onInputChange?: string => void, | |||
onOpen?: void => void, | |||
isLoading?: boolean, | |||
isFavorite?: boolean, | |||
organization?: {} | |||
}; | |||
*/ | |||
interface Props { | |||
property: string; | |||
query: { [x: string]: any }; | |||
options: Array<{ label: string; value: string }>; | |||
onInputChange?: (query: string) => void; | |||
onOpen?: () => void; | |||
isLoading?: boolean; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
} | |||
export default class SearchableFilterFooter extends React.PureComponent { | |||
/*:: props: Props; */ | |||
export default class SearchableFilterFooter extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
handleOptionChange /*: ({ value: string }) => void */ = ({ value }) => { | |||
handleOptionChange = ({ value }: { value: string }) => { | |||
const urlOptions = (this.props.query[this.props.property] || []).concat(value).join(','); | |||
const path = getFilterUrl(this.props, { [this.props.property]: urlOptions }); | |||
this.props.router.push(path); | |||
this.context.router.push(path); | |||
}; | |||
render() { | |||
return ( | |||
<div className="search-navigator-facet-footer projects-facet-footer"> | |||
<Select | |||
onChange={this.handleOptionChange} | |||
className="input-super-large" | |||
placeholder={translate('search_verb')} | |||
clearable={false} | |||
searchable={true} | |||
isLoading={this.props.isLoading} | |||
onChange={this.handleOptionChange} | |||
onInputChange={this.props.onInputChange} | |||
onOpen={this.props.onOpen} | |||
isLoading={this.props.isLoading} | |||
options={this.props.options} | |||
placeholder={translate('search_verb')} | |||
searchable={true} | |||
/> | |||
</div> | |||
); |
@@ -1,34 +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 PropTypes from 'prop-types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class SearchableFilterOption extends React.PureComponent { | |||
static propTypes = { | |||
optionKey: PropTypes.string.isRequired, | |||
option: PropTypes.object | |||
}; | |||
render() { | |||
const optionName = this.props.option ? this.props.option.name : this.props.optionKey; | |||
return <span>{this.props.optionKey !== '<null>' ? optionName : translate('unknown')}</span>; | |||
} | |||
} |
@@ -17,25 +17,15 @@ | |||
* 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 * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
name: string, | |||
children?: React.Element<*> | |||
}; | |||
*/ | |||
export default class FilterHeader extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
option?: { name: string }; | |||
optionKey: string; | |||
} | |||
render() { | |||
return ( | |||
<div className="search-navigator-facet-header projects-facet-header"> | |||
{this.props.name} | |||
{this.props.children} | |||
</div> | |||
); | |||
} | |||
export default function SearchableFilterOption(props: Props) { | |||
const optionName = props.option ? props.option.name : props.optionKey; | |||
return <span>{props.optionKey !== '<null>' ? optionName : translate('unknown')}</span>; | |||
} |
@@ -17,9 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import IssuesFilter from './IssuesFilter'; | |||
export default function SecurityFilter(props) { | |||
interface Props { | |||
className?: string; | |||
headerDetail?: React.ReactNode; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
query: { [x: string]: any }; | |||
} | |||
export default function SecurityFilter(props: Props) { | |||
return <IssuesFilter {...props} name="Security" property="security" />; | |||
} |
@@ -1,77 +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 PropTypes from 'prop-types'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import SizeRating from '../../../components/ui/SizeRating'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSizeRatingLabel, getSizeRatingAverageValue } from '../../../helpers/ratings'; | |||
export default class SizeFilter extends React.PureComponent { | |||
static propTypes = { | |||
className: PropTypes.string, | |||
query: PropTypes.object.isRequired, | |||
isFavorite: PropTypes.bool, | |||
organization: PropTypes.object, | |||
property: PropTypes.string | |||
}; | |||
static defaultProps = { | |||
property: 'size' | |||
}; | |||
getFacetValueForOption(facet, option) { | |||
const map = [ | |||
'*-1000.0', | |||
'1000.0-10000.0', | |||
'10000.0-100000.0', | |||
'100000.0-500000.0', | |||
'500000.0-*' | |||
]; | |||
return facet[map[option - 1]]; | |||
} | |||
renderOption(option, selected) { | |||
return ( | |||
<span> | |||
<SizeRating value={getSizeRatingAverageValue(option)} small={true} muted={!selected} /> | |||
<span className="spacer-left">{getSizeRatingLabel(option)}</span> | |||
</span> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<FilterContainer | |||
property={this.props.property} | |||
className={this.props.className} | |||
options={[1, 2, 3, 4, 5]} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
getFacetValueForOption={this.getFacetValueForOption} | |||
highlightUnder={1} | |||
header={<FilterHeader name={translate('metric_domain.Size')} />} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,67 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import FilterContainer from './FilterContainer'; | |||
import FilterHeader from './FilterHeader'; | |||
import SizeRating from '../../../components/ui/SizeRating'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSizeRatingLabel, getSizeRatingAverageValue } from '../../../helpers/ratings'; | |||
import { Facet } from './Filter'; | |||
export interface Props { | |||
className?: string; | |||
isFavorite?: boolean; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
} | |||
export default function SizeFilter(props: Props) { | |||
const { property = 'size' } = props; | |||
return ( | |||
<FilterContainer | |||
property={property} | |||
className={props.className} | |||
options={[1, 2, 3, 4, 5]} | |||
query={props.query} | |||
renderOption={renderOption} | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
getFacetValueForOption={getFacetValueForOption} | |||
highlightUnder={1} | |||
header={<FilterHeader name={translate('metric_domain.Size')} />} | |||
/> | |||
); | |||
} | |||
function getFacetValueForOption(facet: Facet, option: number) { | |||
const map = ['*-1000.0', '1000.0-10000.0', '10000.0-100000.0', '100000.0-500000.0', '500000.0-*']; | |||
return facet[map[option - 1]]; | |||
} | |||
function renderOption(option: number, selected: boolean) { | |||
return ( | |||
<span> | |||
<SizeRating value={getSizeRatingAverageValue(option)} small={true} muted={!selected} /> | |||
<span className="spacer-left">{getSizeRatingLabel(option)}</span> | |||
</span> | |||
); | |||
} |
@@ -17,66 +17,63 @@ | |||
* 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 * as React from 'react'; | |||
import { debounce, difference, sortBy, size } from 'lodash'; | |||
import Filter from './Filter'; | |||
import Filter, { Facet } from './Filter'; | |||
import FilterHeader from './FilterHeader'; | |||
import SearchableFilterFooter from './SearchableFilterFooter'; | |||
import SearchableFilterOption from './SearchableFilterOption'; | |||
import { searchProjectTags } from '../../../api/components'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
query: {}, | |||
router: { push: ({ pathname: string, query?: {} }) => void }, | |||
value?: Array<string>, | |||
facet?: {}, | |||
isFavorite?: boolean, | |||
organization?: {}, | |||
maxFacetValue?: number | |||
}; | |||
*/ | |||
interface Props { | |||
facet?: Facet; | |||
isFavorite?: boolean; | |||
maxFacetValue?: number; | |||
organization?: { key: string }; | |||
property?: string; | |||
query: { [x: string]: any }; | |||
value?: string[]; | |||
} | |||
/*:: | |||
type State = { | |||
isLoading: boolean, | |||
search: string, | |||
tags: Array<string> | |||
}; | |||
*/ | |||
interface State { | |||
isLoading: boolean; | |||
search: string; | |||
tags: string[]; | |||
} | |||
const LIST_SIZE = 10; | |||
export default class TagsFilter extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
/*:: property: string; */ | |||
export default class TagsFilter extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
constructor(props /*: Props */) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
isLoading: false, | |||
search: '', | |||
tags: [] | |||
}; | |||
this.property = 'tags'; | |||
this.handleSearch = debounce(this.handleSearch.bind(this), 250); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
getSearchOptions( | |||
facet /*: ?{} */, | |||
tags /*: Array<string> */ | |||
) /*: Array<{ label: string, value: string }> */ { | |||
let tagsCopy = [...tags]; | |||
if (facet) { | |||
tagsCopy = difference(tagsCopy, Object.keys(facet)); | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
getSearchOptions = () => { | |||
let tagsCopy = [...this.state.tags]; | |||
if (this.props.facet) { | |||
tagsCopy = difference(tagsCopy, Object.keys(this.props.facet)); | |||
} | |||
return tagsCopy.slice(0, LIST_SIZE).map(tag => ({ label: tag, value: tag })); | |||
} | |||
}; | |||
handleSearch = (search /*: ?string */) => { | |||
handleSearch = (search?: string) => { | |||
if (search !== this.state.search) { | |||
search = search || ''; | |||
this.setState({ search, isLoading: true }); | |||
@@ -84,23 +81,26 @@ export default class TagsFilter extends React.PureComponent { | |||
q: search, | |||
ps: size(this.props.facet || {}) + LIST_SIZE | |||
}).then(result => { | |||
this.setState({ isLoading: false, tags: result.tags }); | |||
if (this.mounted) { | |||
this.setState({ isLoading: false, tags: result.tags }); | |||
} | |||
}); | |||
} | |||
}; | |||
getSortedOptions(facet /*: {} */ = {}) { | |||
return sortBy(Object.keys(facet), [option => -facet[option], option => option]); | |||
} | |||
getSortedOptions = (facet: Facet = {}) => | |||
sortBy(Object.keys(facet), [(option: string) => -facet[option], (option: string) => option]); | |||
getFacetValueForOption = (facet /*: {} */, option /*: string */) => facet[option]; | |||
getFacetValueForOption = (facet: Facet = {}, option: string) => facet[option]; | |||
renderOption = (option /*: string */) => <SearchableFilterOption optionKey={option} />; | |||
renderOption = (option: string) => <SearchableFilterOption optionKey={option} />; | |||
render() { | |||
const { property = 'tags' } = this.props; | |||
return ( | |||
<Filter | |||
property={this.property} | |||
property={property} | |||
options={this.getSortedOptions(this.props.facet)} | |||
query={this.props.query} | |||
renderOption={this.renderOption} | |||
@@ -113,15 +113,14 @@ export default class TagsFilter extends React.PureComponent { | |||
header={<FilterHeader name={translate('projects.facets.tags')} />} | |||
footer={ | |||
<SearchableFilterFooter | |||
property={this.property} | |||
query={this.props.query} | |||
options={this.getSearchOptions(this.props.facet, this.state.tags)} | |||
isFavorite={this.props.isFavorite} | |||
isLoading={this.state.isLoading} | |||
onOpen={this.handleSearch} | |||
onInputChange={this.handleSearch} | |||
isFavorite={this.props.isFavorite} | |||
onOpen={this.handleSearch} | |||
organization={this.props.organization} | |||
router={this.props.router} | |||
options={this.getSearchOptions()} | |||
property={property} | |||
query={this.props.query} | |||
/> | |||
} | |||
/> |
@@ -18,16 +18,15 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import TagsFilter from './TagsFilter'; | |||
import { | |||
getProjectsAppFacetByProperty, | |||
getProjectsAppMaxFacetValue | |||
} from '../../../store/rootReducer'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
const mapStateToProps = (state: any, ownProps: any) => ({ | |||
value: ownProps.query['tags'], | |||
facet: getProjectsAppFacetByProperty(state, 'tags'), | |||
maxFacetValue: getProjectsAppMaxFacetValue(state) | |||
}); | |||
export default connect(mapStateToProps)(withRouter(TagsFilter)); | |||
export default connect<any, any, any>(mapStateToProps)(TagsFilter); |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import CoverageFilter from '../CoverageFilter'; | |||
it('renders', () => { | |||
const wrapper = shallow(<CoverageFilter query={{}} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot(); | |||
const getFacetValueForOption = wrapper.prop('getFacetValueForOption'); | |||
expect( | |||
getFacetValueForOption( | |||
{ '80.0-*': 1, '70.0-80.0': 42, '50.0-70.0': 14, '30.0-50.0': 13, '*-30.0': 8, NO_DATA: 3 }, | |||
2 | |||
) | |||
).toBe(42); | |||
}); |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import DuplicationsFilter from '../DuplicationsFilter'; | |||
it('renders', () => { | |||
const wrapper = shallow(<DuplicationsFilter query={{}} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
const renderOption = wrapper.prop('renderOption'); | |||
expect(renderOption(2, false)).toMatchSnapshot(); | |||
expect(renderOption(6, true)).toMatchSnapshot(); | |||
const getFacetValueForOption = wrapper.prop('getFacetValueForOption'); | |||
expect( | |||
getFacetValueForOption( | |||
{ '*-3.0': 1, '3.0-5.0': 42, '5.0-10.0': 14, '10.0-20.0': 13, '20.0-*': 8, NO_DATA: 3 }, | |||
2 | |||
) | |||
).toBe(42); | |||
}); |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import Filter from '../Filter'; | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('renders header and footer', () => { | |||
expect(shallowRender({ header: <header />, footer: <footer /> })).toMatchSnapshot(); | |||
}); | |||
it('renders no results', () => { | |||
expect(shallowRender({ options: [] })).toMatchSnapshot(); | |||
}); | |||
it('highlights under', () => { | |||
expect(shallowRender({ highlightUnder: 1 })).toMatchSnapshot(); | |||
}); | |||
it('renders selected', () => { | |||
expect(shallowRender({ value: 2 })).toMatchSnapshot(); | |||
}); | |||
it('hightlights under selected', () => { | |||
expect(shallowRender({ highlightUnder: 1, value: 2 })).toMatchSnapshot(); | |||
}); | |||
it('renders multiple selected', () => { | |||
expect(shallowRender({ value: [1, 2] })).toMatchSnapshot(); | |||
}); | |||
it('renders facet bar chart', () => { | |||
expect( | |||
shallowRender({ | |||
getFacetValueForOption: (facet: any, option: any) => facet[option], | |||
facet: { a: 17, b: 15, c: 24 }, | |||
maxFacetValue: 24, | |||
options: ['a', 'b', 'c'] | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props?: any) { | |||
return shallow( | |||
<Filter | |||
options={[1, 2, 3]} | |||
property="foo" | |||
query={{}} | |||
renderOption={option => option} | |||
{...props} | |||
/> | |||
); | |||
} |