diff options
19 files changed, 849 insertions, 168 deletions
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index e848e7657de..55e16b34019 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js index 67bc0accfff..3b65bfd5d8d 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js new file mode 100644 index 00000000000..ca31ad7fbe9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js new file mode 100644 index 00000000000..bef22ae59a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js new file mode 100644 index 00000000000..b506baf520d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js new file mode 100644 index 00000000000..a1a49c3ea84 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelectOption-test.js index 5d9c801df75..b19fabe98bc 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelectOption-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js new file mode 100644 index 00000000000..b8667a28700 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap new file mode 100644 index 00000000000..8939575929b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap new file mode 100644 index 00000000000..794349a21ca --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap new file mode 100644 index 00000000000..ce43bf57d1f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap deleted file mode 100644 index 9a7d8d66e31..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap +++ /dev/null @@ -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" -/> -`; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index abc060f17ba..d413f3d0cb6 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/projects/utils.js b/server/sonar-web/src/main/js/apps/projects/utils.js index d383b8cc388..9fe7815798c 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.js +++ b/server/sonar-web/src/main/js/apps/projects/utils.js @@ -47,6 +47,8 @@ export const saveAll = () => save(LOCALSTORAGE_ALL); export const saveFavorite = () => save(LOCALSTORAGE_FAVORITE); +export const VIEWS = ['overall']; + export const VISUALIZATIONS = [ 'risk', 'reliability', diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js index eccc643a5fd..cc7627f740f 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js b/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js index 429b88e2a9f..0374c7b7f04 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js +++ b/server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js @@ -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> + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js b/server/sonar-web/src/main/js/components/icons-components/CloseIcon.js index 04824d39d86..8722eba0318 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js +++ b/server/sonar-web/src/main/js/components/icons-components/CloseIcon.js @@ -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> + ); } diff --git a/server/sonar-web/src/main/js/components/icons-components/ListIcon.js b/server/sonar-web/src/main/js/components/icons-components/ListIcon.js new file mode 100644 index 00000000000..0a4077cc5db --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/ListIcon.js @@ -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> + ); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f863c18f716..3da60b5a012 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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. |