@@ -17,9 +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 Helmet from 'react-helmet'; | |||
import PageHeaderContainer from './PageHeaderContainer'; | |||
import ProjectOptionBar from './ProjectOptionBar'; | |||
import ProjectsListContainer from './ProjectsListContainer'; | |||
import ProjectsListFooterContainer from './ProjectsListFooterContainer'; | |||
import PageSidebar from './PageSidebar'; | |||
@@ -28,112 +30,122 @@ import { parseUrlQuery } from '../store/utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../styles.css'; | |||
export default class AllProjects extends React.PureComponent { | |||
static propTypes = { | |||
isFavorite: React.PropTypes.bool.isRequired, | |||
location: React.PropTypes.object.isRequired, | |||
fetchProjects: React.PropTypes.func.isRequired, | |||
organization: React.PropTypes.object, | |||
router: React.PropTypes.object.isRequired | |||
}; | |||
type Props = { | |||
isFavorite: boolean, | |||
location: { pathname: string, query: { [string]: string } }, | |||
fetchProjects: (query: string, isFavorite: boolean, organization?: {}) => Promise<*>, | |||
organization?: { key: string }, | |||
router: { push: ({ pathname: string, query?: {} }) => void } | |||
}; | |||
type State = { | |||
query: { [string]: string }, | |||
optionBarOpen: boolean | |||
}; | |||
state = { | |||
query: {} | |||
export default class AllProjects extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
query: {}, | |||
optionBarOpen: false | |||
}; | |||
componentDidMount() { | |||
this.handleQueryChange(); | |||
document.getElementById('footer').classList.add('search-navigator-footer'); | |||
const footer = document.getElementById('footer'); | |||
footer && footer.classList.add('search-navigator-footer'); | |||
} | |||
componentDidUpdate(prevProps) { | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.location.query !== this.props.location.query) { | |||
this.handleQueryChange(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
document.getElementById('footer').classList.remove('search-navigator-footer'); | |||
const footer = document.getElementById('footer'); | |||
footer && footer.classList.remove('search-navigator-footer'); | |||
} | |||
handleQueryChange() { | |||
const query = parseUrlQuery(this.props.location.query); | |||
this.setState({ query }); | |||
this.props.fetchProjects(query, this.props.isFavorite, this.props.organization); | |||
} | |||
handleViewChange = view => { | |||
const query = { | |||
...this.props.location.query, | |||
view: view === 'list' ? undefined : view | |||
}; | |||
if (query.view !== 'visualizations') { | |||
Object.assign(query, { visualization: undefined }); | |||
} | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query | |||
}); | |||
openOptionBar = (evt: Event & { currentTarget: HTMLElement }) => { | |||
evt.currentTarget.blur(); | |||
evt.preventDefault(); | |||
this.handleOptionBarToggle(true); | |||
}; | |||
handleVisualizationChange = visualization => { | |||
handleOptionBarToggle = (open: boolean) => this.setState({ optionBarOpen: open }); | |||
handlePerspectiveChange = ({ view, visualization }: { view: string, visualization?: string }) => { | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...this.props.location.query, | |||
view: 'visualizations', | |||
view: view === 'overall' ? undefined : view, | |||
visualization | |||
} | |||
}); | |||
}; | |||
handleQueryChange() { | |||
const query = parseUrlQuery(this.props.location.query); | |||
this.setState({ query }); | |||
this.props.fetchProjects(query, this.props.isFavorite, this.props.organization); | |||
} | |||
render() { | |||
const { query } = this.state; | |||
const { query, optionBarOpen } = this.state; | |||
const isFiltered = Object.keys(query).some(key => query[key] != null); | |||
const view = query.view || 'list'; | |||
const view = query.view || 'overall'; | |||
const visualization = query.visualization || 'risk'; | |||
const top = this.props.organization ? 95 : 30; | |||
const top = (this.props.organization ? 95 : 30) + (optionBarOpen ? 45 : 0); | |||
return ( | |||
<div className="layout-page projects-page"> | |||
<div> | |||
<Helmet title={translate('projects.page')} /> | |||
<div className="layout-page-side-outer"> | |||
<div className="layout-page-side" style={{ top }}> | |||
<div className="layout-page-side-inner"> | |||
<div className="layout-page-filters"> | |||
<PageSidebar | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/> | |||
<ProjectOptionBar | |||
onPerspectiveChange={this.handlePerspectiveChange} | |||
onToggleOptionBar={this.handleOptionBarToggle} | |||
open={optionBarOpen} | |||
view={view} | |||
visualization={visualization} | |||
/> | |||
<div className="layout-page projects-page"> | |||
<div className="layout-page-side-outer"> | |||
<div className="layout-page-side projects-page-side" style={{ top }}> | |||
<div className="layout-page-side-inner"> | |||
<div className="layout-page-filters"> | |||
<PageSidebar | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div className="layout-page-main"> | |||
<div className="layout-page-main-inner"> | |||
<PageHeaderContainer onViewChange={this.handleViewChange} view={view} /> | |||
{view === 'list' && | |||
<ProjectsListContainer | |||
isFavorite={this.props.isFavorite} | |||
isFiltered={isFiltered} | |||
organization={this.props.organization} | |||
/>} | |||
{view === 'list' && | |||
<ProjectsListFooterContainer | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/>} | |||
{view === 'visualizations' && | |||
<VisualizationsContainer | |||
onVisualizationChange={this.handleVisualizationChange} | |||
sort={query.sort} | |||
visualization={visualization} | |||
/>} | |||
<div className="layout-page-main"> | |||
<div className="layout-page-main-inner"> | |||
<PageHeaderContainer onOpenOptionBar={this.openOptionBar} /> | |||
{view === 'overall' && | |||
<ProjectsListContainer | |||
isFavorite={this.props.isFavorite} | |||
isFiltered={isFiltered} | |||
organization={this.props.organization} | |||
/>} | |||
{view === 'overall' && | |||
<ProjectsListFooterContainer | |||
query={query} | |||
isFavorite={this.props.isFavorite} | |||
organization={this.props.organization} | |||
/>} | |||
{view === 'visualizations' && | |||
<VisualizationsContainer sort={query.sort} visualization={visualization} />} | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@@ -19,22 +19,24 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import ViewSelect from './ViewSelect'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
loading: boolean, | |||
onViewChange: string => void, | |||
total?: number, | |||
view: string | |||
onOpenOptionBar: () => void, | |||
total?: number | |||
}; | |||
export default function PageHeader(props: Props) { | |||
return ( | |||
<header className="page-header"> | |||
<ViewSelect onChange={props.onViewChange} view={props.view} /> | |||
<div className="page-actions projects-page-actions"> | |||
<div className="text-right spacer-bottom"> | |||
<a className="button" href="#" onClick={props.onOpenOptionBar}> | |||
{translate('projects.view_settings')} | |||
</a> | |||
</div> | |||
{!!props.loading && <i className="spinner spacer-right" />} | |||
{props.total != null && |
@@ -0,0 +1,81 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import PerspectiveSelectOption from './PerspectiveSelectOption'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { VIEWS, VISUALIZATIONS } from '../utils'; | |||
export type Option = { label: string, type: string, value: string }; | |||
type Props = { | |||
onChange: ({ view: string, visualization?: string }) => void, | |||
view: string, | |||
visualization?: string | |||
}; | |||
export default class PerspectiveSelect extends React.PureComponent { | |||
options: Array<Option>; | |||
props: Props; | |||
constructor(props: Props) { | |||
super(props); | |||
this.options = [ | |||
...VIEWS.map(opt => ({ | |||
type: 'view', | |||
value: opt, | |||
label: translate('projects.view', opt) | |||
})), | |||
...VISUALIZATIONS.map(opt => ({ | |||
type: 'visualization', | |||
value: opt, | |||
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> | |||
<label>{translate('projects.perspective')}:</label> | |||
<Select | |||
className="little-spacer-left input-medium" | |||
clearable={false} | |||
onChange={this.handleChange} | |||
optionComponent={PerspectiveSelectOption} | |||
options={this.options} | |||
searchable={false} | |||
value={perspective} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import 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 default class PerspectiveSelectOption extends React.PureComponent { | |||
props: Props; | |||
handleMouseDown = (event: MouseEvent) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onSelect(this.props.option, event); | |||
}; | |||
handleMouseEnter = (event: MouseEvent) => { | |||
this.props.onFocus(this.props.option, event); | |||
}; | |||
handleMouseMove = (event: MouseEvent) => { | |||
if (this.props.isFocused) { | |||
return; | |||
} | |||
this.props.onFocus(this.props.option, event); | |||
}; | |||
render() { | |||
const { option } = this.props; | |||
return ( | |||
<div | |||
className={this.props.className} | |||
onMouseDown={this.handleMouseDown} | |||
onMouseEnter={this.handleMouseEnter} | |||
onMouseMove={this.handleMouseMove} | |||
title={option.label}> | |||
<div> | |||
{option.type === 'view' && <ListIcon className="little-spacer-right" />} | |||
{option.type === 'visualization' && <BubblesIcon className="little-spacer-right" />} | |||
{this.props.children} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import CloseIcon from '../../../components/icons-components/CloseIcon'; | |||
import PerspectiveSelect from './PerspectiveSelect'; | |||
type Props = { | |||
onPerspectiveChange: ({ view: string, visualization?: string }) => void, | |||
onToggleOptionBar: boolean => void, | |||
open: boolean, | |||
view: string, | |||
visualization?: string | |||
}; | |||
export default class ProjectOptionBar extends React.PureComponent { | |||
props: Props; | |||
closeBar = (evt: Event & { currentTarget: HTMLElement }) => { | |||
evt.currentTarget.blur(); | |||
evt.preventDefault(); | |||
this.props.onToggleOptionBar(false); | |||
}; | |||
render() { | |||
const { open } = this.props; | |||
return ( | |||
<div className="projects-topbar"> | |||
<div className={classNames('projects-topbar-actions', { open })}> | |||
<a | |||
className="projects-topbar-button projects-topbar-button-close" | |||
href="#" | |||
onClick={this.closeBar}> | |||
<CloseIcon /> | |||
</a> | |||
<div className="projects-topbar-actions-inner"> | |||
<PerspectiveSelect | |||
onChange={this.props.onPerspectiveChange} | |||
view={this.props.view} | |||
visualization={this.props.visualization} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PerspectiveSelect from '../PerspectiveSelect'; | |||
it('should render correctly', () => { | |||
expect(shallow(<PerspectiveSelect view="overall" />)).toMatchSnapshot(); | |||
}); | |||
it('should render with coverage selected', () => { | |||
expect( | |||
shallow(<PerspectiveSelect view="visualizations" visualization="coverage" />) | |||
).toMatchSnapshot(); | |||
}); | |||
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' }); | |||
expect(onChange.mock.calls).toMatchSnapshot(); | |||
}); |
@@ -19,8 +19,25 @@ | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ViewSelect from '../ViewSelect'; | |||
import PerspectiveSelectOption from '../PerspectiveSelectOption'; | |||
it('should render options', () => { | |||
expect(shallow(<ViewSelect view="visualizations" />)).toMatchSnapshot(); | |||
it('should render correctly for a view', () => { | |||
expect( | |||
shallow( | |||
<PerspectiveSelectOption option={{ value: 'overall', type: 'view', label: 'Overall' }}> | |||
Overall | |||
</PerspectiveSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render correctly for a visualization', () => { | |||
expect( | |||
shallow( | |||
<PerspectiveSelectOption | |||
option={{ value: 'coverage', type: 'visualization', label: 'Coverage' }}> | |||
Coverage | |||
</PerspectiveSelectOption> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectOptionBar from '../ProjectOptionBar'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render option bar closed', () => { | |||
expect(shallow(<ProjectOptionBar open={false} view="overall" />)).toMatchSnapshot(); | |||
}); | |||
it('should render option bar open', () => { | |||
expect( | |||
shallow(<ProjectOptionBar open={true} view="visualizations" visualization="coverage" />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should call close method correctly', () => { | |||
const toggle = jest.fn(); | |||
const wrapper = shallow(<ProjectOptionBar open={true} view="leak" onToggleOptionBar={toggle} />); | |||
click(wrapper.find('a.projects-topbar-button-close')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,202 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should handle perspective change correctly 1`] = ` | |||
Array [ | |||
Array [ | |||
Object { | |||
"view": "overall", | |||
}, | |||
], | |||
Array [ | |||
Object { | |||
"view": "leak", | |||
}, | |||
], | |||
Array [ | |||
Object { | |||
"view": "visualizations", | |||
"visualization": "coverage", | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<div> | |||
<label> | |||
projects.perspective | |||
: | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
className="little-spacer-left input-medium" | |||
clearAllText="Clear all" | |||
clearValueText="Clear value" | |||
clearable={false} | |||
delimiter="," | |||
disabled={false} | |||
escapeClearsValue={true} | |||
filterOptions={[Function]} | |||
ignoreAccents={true} | |||
ignoreCase={true} | |||
inputProps={Object {}} | |||
isLoading={false} | |||
joinValues={false} | |||
labelKey="label" | |||
matchPos="any" | |||
matchProp="any" | |||
menuBuffer={0} | |||
menuRenderer={[Function]} | |||
multi={false} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
openAfterFocus={false} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "projects.view.overall", | |||
"type": "view", | |||
"value": "overall", | |||
}, | |||
Object { | |||
"label": "projects.visualization.risk", | |||
"type": "visualization", | |||
"value": "risk", | |||
}, | |||
Object { | |||
"label": "projects.visualization.reliability", | |||
"type": "visualization", | |||
"value": "reliability", | |||
}, | |||
Object { | |||
"label": "projects.visualization.security", | |||
"type": "visualization", | |||
"value": "security", | |||
}, | |||
Object { | |||
"label": "projects.visualization.maintainability", | |||
"type": "visualization", | |||
"value": "maintainability", | |||
}, | |||
Object { | |||
"label": "projects.visualization.coverage", | |||
"type": "visualization", | |||
"value": "coverage", | |||
}, | |||
Object { | |||
"label": "projects.visualization.duplications", | |||
"type": "visualization", | |||
"value": "duplications", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="overall" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
`; | |||
exports[`should render with coverage selected 1`] = ` | |||
<div> | |||
<label> | |||
projects.perspective | |||
: | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
className="little-spacer-left input-medium" | |||
clearAllText="Clear all" | |||
clearValueText="Clear value" | |||
clearable={false} | |||
delimiter="," | |||
disabled={false} | |||
escapeClearsValue={true} | |||
filterOptions={[Function]} | |||
ignoreAccents={true} | |||
ignoreCase={true} | |||
inputProps={Object {}} | |||
isLoading={false} | |||
joinValues={false} | |||
labelKey="label" | |||
matchPos="any" | |||
matchProp="any" | |||
menuBuffer={0} | |||
menuRenderer={[Function]} | |||
multi={false} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
openAfterFocus={false} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "projects.view.overall", | |||
"type": "view", | |||
"value": "overall", | |||
}, | |||
Object { | |||
"label": "projects.visualization.risk", | |||
"type": "visualization", | |||
"value": "risk", | |||
}, | |||
Object { | |||
"label": "projects.visualization.reliability", | |||
"type": "visualization", | |||
"value": "reliability", | |||
}, | |||
Object { | |||
"label": "projects.visualization.security", | |||
"type": "visualization", | |||
"value": "security", | |||
}, | |||
Object { | |||
"label": "projects.visualization.maintainability", | |||
"type": "visualization", | |||
"value": "maintainability", | |||
}, | |||
Object { | |||
"label": "projects.visualization.coverage", | |||
"type": "visualization", | |||
"value": "coverage", | |||
}, | |||
Object { | |||
"label": "projects.visualization.duplications", | |||
"type": "visualization", | |||
"value": "duplications", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="coverage" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
`; |
@@ -0,0 +1,33 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly for a view 1`] = ` | |||
<div | |||
onMouseDown={[Function]} | |||
onMouseEnter={[Function]} | |||
onMouseMove={[Function]} | |||
title="Overall" | |||
> | |||
<div> | |||
<ListIcon | |||
className="little-spacer-right" | |||
/> | |||
Overall | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly for a visualization 1`] = ` | |||
<div | |||
onMouseDown={[Function]} | |||
onMouseEnter={[Function]} | |||
onMouseMove={[Function]} | |||
title="Coverage" | |||
> | |||
<div> | |||
<BubblesIcon | |||
className="little-spacer-right" | |||
/> | |||
Coverage | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,60 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should call close method correctly 1`] = ` | |||
Array [ | |||
Array [ | |||
false, | |||
], | |||
] | |||
`; | |||
exports[`should render option bar closed 1`] = ` | |||
<div | |||
className="projects-topbar" | |||
> | |||
<div | |||
className="projects-topbar-actions" | |||
> | |||
<a | |||
className="projects-topbar-button projects-topbar-button-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<CloseIcon /> | |||
</a> | |||
<div | |||
className="projects-topbar-actions-inner" | |||
> | |||
<PerspectiveSelect | |||
view="overall" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render option bar open 1`] = ` | |||
<div | |||
className="projects-topbar" | |||
> | |||
<div | |||
className="projects-topbar-actions open" | |||
> | |||
<a | |||
className="projects-topbar-button projects-topbar-button-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<CloseIcon /> | |||
</a> | |||
<div | |||
className="projects-topbar-actions-inner" | |||
> | |||
<PerspectiveSelect | |||
view="visualizations" | |||
visualization="coverage" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -1,22 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render options 1`] = ` | |||
<RadioToggle | |||
disabled={false} | |||
name="view" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "projects.view.list", | |||
"value": "list", | |||
}, | |||
Object { | |||
"label": "projects.view.visualizations", | |||
"value": "visualizations", | |||
}, | |||
] | |||
} | |||
value="visualizations" | |||
/> | |||
`; |
@@ -2,6 +2,67 @@ | |||
margin-bottom: 0; | |||
} | |||
.projects-page-side { | |||
transition: top 150ms ease-out; | |||
} | |||
.projects-topbar { | |||
position: fixed; | |||
width: 100%; | |||
z-index: 100; | |||
} | |||
.projects-topbar-actions { | |||
box-sizing: border-box; | |||
position: absolute; | |||
left: 0; | |||
right: 0; | |||
top: -50px; | |||
display: flex; | |||
flex-direction: row-reverse; | |||
z-index: 50; | |||
flex-grow: 0.000001; | |||
background-color: #fff; | |||
transition: top 150ms ease-out; | |||
} | |||
.projects-topbar-actions.open { | |||
top: 0; | |||
} | |||
.projects-topbar-actions-inner { | |||
display: flex; | |||
flex-wrap: nowrap; | |||
justify-content: center; | |||
align-items: center; | |||
flex-grow: 1; | |||
border-bottom: 1px solid #e6e6e6; | |||
} | |||
.projects-topbar-button { | |||
box-sizing: border-box; | |||
float: right; | |||
padding: 11px; | |||
border: none; | |||
width: 46px; | |||
height: 46px; | |||
color: #777; | |||
text-align: center; | |||
text-decoration: none; | |||
cursor: pointer; | |||
} | |||
.projects-topbar-button-close { | |||
padding-top: 15px; | |||
border-left: 1px solid #e6e6e6; | |||
border-bottom: 1px solid #e6e6e6; | |||
} | |||
.projects-topbar-button:hover, .project-topbar-button:focus, .project-topbar-button:active { | |||
color: #444; | |||
outline: none; | |||
} | |||
.projects-sidebar { | |||
width: 260px; | |||
} | |||
@@ -175,8 +236,6 @@ | |||
.projects-visualization { | |||
position: relative; | |||
height: 600px; | |||
margin-top: 15px; | |||
border-top: 1px solid #e6e6e6; | |||
} | |||
.projects-visualization .measure-details-bubble-chart-axis.y { |
@@ -47,6 +47,8 @@ export const saveAll = () => save(LOCALSTORAGE_ALL); | |||
export const saveFavorite = () => save(LOCALSTORAGE_FAVORITE); | |||
export const VIEWS = ['overall']; | |||
export const VISUALIZATIONS = [ | |||
'risk', | |||
'reliability', |
@@ -19,7 +19,6 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import VisualizationsHeader from './VisualizationsHeader'; | |||
import Risk from './Risk'; | |||
import Reliability from './Reliability'; | |||
import Security from './Security'; | |||
@@ -32,7 +31,6 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default class Visualizations extends React.PureComponent { | |||
props: { | |||
displayOrganizations: boolean, | |||
onVisualizationChange: string => void, | |||
projects?: Array<*>, | |||
sort?: string, | |||
total?: number, | |||
@@ -81,14 +79,8 @@ export default class Visualizations extends React.PureComponent { | |||
return ( | |||
<div className="boxed-group projects-visualizations"> | |||
<VisualizationsHeader | |||
onVisualizationChange={this.props.onVisualizationChange} | |||
visualization={this.props.visualization} | |||
/> | |||
<div className="projects-visualization"> | |||
<div> | |||
{projects != null && this.renderVisualization(projects)} | |||
</div> | |||
{projects != null && this.renderVisualization(projects)} | |||
</div> | |||
{this.renderFooter()} | |||
</div> |
@@ -19,32 +19,29 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import RadioToggle from '../../../components/controls/RadioToggle'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class ViewSelect extends React.PureComponent { | |||
props: { | |||
onChange: string => void, | |||
view: string | |||
}; | |||
type Props = { className?: string, size?: number }; | |||
handleChange = (view: string) => { | |||
this.props.onChange(view); | |||
}; | |||
render() { | |||
const options = ['list', 'visualizations'].map(option => ({ | |||
value: option, | |||
label: translate('projects.view', option) | |||
})); | |||
return ( | |||
<RadioToggle | |||
name="view" | |||
onCheck={this.handleChange} | |||
options={options} | |||
value={this.props.view} | |||
export default function BubblesIcon({ className, size = 16 }: Props) { | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<circle | |||
cx="4" | |||
cy="12" | |||
r="3.5" | |||
style={{ fill: 'none', stroke: 'currentColor', strokeMiterlimit: 10 }} | |||
/> | |||
<circle | |||
cx="10.4" | |||
cy="5.6" | |||
r="5.1" | |||
style={{ fill: 'none', stroke: 'currentColor', strokeMiterlimit: 10 }} | |||
/> | |||
); | |||
} | |||
</svg> | |||
); | |||
} |
@@ -19,37 +19,22 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { VISUALIZATIONS } from '../utils'; | |||
export default class VisualizationsHeader extends React.PureComponent { | |||
props: { | |||
onVisualizationChange: string => void, | |||
visualization: string | |||
}; | |||
type Props = { className?: string, size?: number }; | |||
handleChange = (option: { value: string }) => { | |||
this.props.onVisualizationChange(option.value); | |||
}; | |||
render() { | |||
const options = VISUALIZATIONS.map(option => ({ | |||
value: option, | |||
label: translate('projects.visualization', option) | |||
})); | |||
return ( | |||
<header className="boxed-group-header"> | |||
<Select | |||
className="input-medium" | |||
clearable={false} | |||
onChange={this.handleChange} | |||
options={options} | |||
searchable={false} | |||
value={this.props.visualization} | |||
/> | |||
</header> | |||
); | |||
} | |||
export default function CloseIcon({ className, size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<path | |||
fill="currentColor" | |||
d="M12.843 11.232q0 0.357-0.25 0.607l-1.214 1.214q-0.25 0.25-0.607 0.25t-0.607-0.25l-2.625-2.625-2.625 2.625q-0.25 0.25-0.607 0.25t-0.607-0.25l-1.214-1.214q-0.25-0.25-0.25-0.607t0.25-0.607l2.625-2.625-2.625-2.625q-0.25-0.25-0.25-0.607t0.25-0.607l1.214-1.214q0.25-0.25 0.607-0.25t0.607 0.25l2.625 2.625 2.625-2.625q0.25-0.25 0.607-0.25t0.607 0.25l1.214 1.214q0.25 0.25 0.25 0.607t-0.25 0.607l-2.625 2.625 2.625 2.625q0.25 0.25 0.25 0.607z" | |||
/> | |||
</svg> | |||
); | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
type Props = { className?: string, size?: number }; | |||
export default function ListIcon({ className, size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<path | |||
fill="currentColor" | |||
d="M16 12v1.143q0 0.232-0.17 0.402t-0.402 0.17h-14.857q-0.232 0-0.402-0.17t-0.17-0.402v-1.143q0-0.232 0.17-0.402t0.402-0.17h14.857q0.232 0 0.402 0.17t0.17 0.402zM16 8.571v1.143q0 0.232-0.17 0.402t-0.402 0.17h-14.857q-0.232 0-0.402-0.17t-0.17-0.402v-1.143q0-0.232 0.17-0.402t0.402-0.17h14.857q0.232 0 0.402 0.17t0.17 0.402zM16 5.143v1.143q0 0.232-0.17 0.402t-0.402 0.17h-14.857q-0.232 0-0.402-0.17t-0.17-0.402v-1.143q0-0.232 0.17-0.402t0.402-0.17h14.857q0.232 0 0.402 0.17t0.17 0.402zM16 1.714v1.143q0 0.232-0.17 0.402t-0.402 0.17h-14.857q-0.232 0-0.402-0.17t-0.17-0.402v-1.143q0-0.232 0.17-0.402t0.402-0.17h14.857q0.232 0 0.402 0.17t0.17 0.402z" | |||
/> | |||
</svg> | |||
); | |||
} |
@@ -862,8 +862,9 @@ projects.explore_projects=Explore Projects | |||
projects.not_analyzed=Project is not analyzed yet. | |||
projects.search=Search by project name or key | |||
projects.sort_list=Sort list by | |||
projects.view.list=List | |||
projects.view.visualizations=Visualizations | |||
projects.perspective=Perspective | |||
projects.view_settings=Settings | |||
projects.view.overall=Overall Status | |||
projects.worse_of_reliablity_and_security=Worse of Reliability and Security | |||
projects.visualization.risk=Risk | |||
projects.visualization.risk.description=Get quick insights into the operational risks in your projects. Any color but green indicates immediate risks: Bugs or Vulnerabilities that should be examined. A position at the top or right of the graph means that the longer-term health of the project may be at risk. Green bubbles at the bottom-left are best. |