Browse Source

SONAR-9253 Create an option panel on top of projects page

tags/6.5-M1
Grégoire Aubert 7 years ago
parent
commit
c26d1c37fd
19 changed files with 849 additions and 168 deletions
  1. 80
    68
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  2. 8
    6
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
  3. 81
    0
      server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js
  4. 72
    0
      server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js
  5. 65
    0
      server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js
  6. 43
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js
  7. 20
    3
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelectOption-test.js
  8. 40
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js
  9. 202
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap
  10. 33
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap
  11. 60
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap
  12. 0
    22
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap
  13. 61
    2
      server/sonar-web/src/main/js/apps/projects/styles.css
  14. 2
    0
      server/sonar-web/src/main/js/apps/projects/utils.js
  15. 1
    9
      server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js
  16. 22
    25
      server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js
  17. 16
    31
      server/sonar-web/src/main/js/components/icons-components/CloseIcon.js
  18. 40
    0
      server/sonar-web/src/main/js/components/icons-components/ListIcon.js
  19. 3
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 80
- 68
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js View File

@@ -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>

+ 8
- 6
server/sonar-web/src/main/js/apps/projects/components/PageHeader.js View File

@@ -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 &&

+ 81
- 0
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js View File

@@ -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>
);
}
}

+ 72
- 0
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelectOption.js View File

@@ -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>
);
}
}

+ 65
- 0
server/sonar-web/src/main/js/apps/projects/components/ProjectOptionBar.js View File

@@ -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>
);
}
}

+ 43
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.js View File

@@ -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();
});

server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js → server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelectOption-test.js View File

@@ -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();
});

+ 40
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js View File

@@ -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();
});

+ 202
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.js.snap View File

@@ -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>
`;

+ 33
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelectOption-test.js.snap View File

@@ -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>
`;

+ 60
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap View File

@@ -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>
`;

+ 0
- 22
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap View File

@@ -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"
/>
`;

+ 61
- 2
server/sonar-web/src/main/js/apps/projects/styles.css View File

@@ -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 {

+ 2
- 0
server/sonar-web/src/main/js/apps/projects/utils.js View File

@@ -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',

+ 1
- 9
server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js View File

@@ -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>

server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js → server/sonar-web/src/main/js/components/icons-components/BubblesIcon.js View File

@@ -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>
);
}

server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js → server/sonar-web/src/main/js/components/icons-components/CloseIcon.js View File

@@ -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>
);
}

+ 40
- 0
server/sonar-web/src/main/js/components/icons-components/ListIcon.js View File

@@ -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>
);
}

+ 3
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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.

Loading…
Cancel
Save