aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.js148
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeader.js14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js81
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js72
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js65
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js43
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelectOption-test.js (renamed from server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js)23
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js40
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap202
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap60
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css63
-rw-r--r--server/sonar-web/src/main/js/apps/projects/utils.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js10
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js (renamed from server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js)47
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/CloseIcon.js (renamed from server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js)47
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/ListIcon.js40
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties5
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.