Browse Source

SONAR-9065 Display concise issues list when browsing code (#1953)

tags/6.4-RC1
Stas Vilchik 7 years ago
parent
commit
b27171e264
27 changed files with 1029 additions and 165 deletions
  1. 95
    44
      server/sonar-web/src/main/js/apps/issues/components/App.js
  2. 0
    106
      server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
  3. 41
    0
      server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js
  4. 4
    13
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  5. 51
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js
  6. 49
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js
  7. 54
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
  8. 34
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js
  9. 42
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js
  10. 57
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js
  11. 84
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
  12. 49
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
  13. 51
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js
  14. 41
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js
  15. 29
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js
  16. 27
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js
  17. 50
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js
  18. 27
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js
  19. 9
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap
  20. 6
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap
  21. 11
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap
  22. 27
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap
  23. 26
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap
  24. 126
    0
      server/sonar-web/src/main/js/apps/issues/styles.css
  25. 2
    2
      server/sonar-web/src/main/js/components/layout/PageSide.js
  26. 36
    0
      server/sonar-web/src/main/js/components/shared/TypeHelper.js
  27. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 95
- 44
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -22,7 +22,6 @@ import React from 'react';
import Helmet from 'react-helmet';
import key from 'keymaster';
import { keyBy, without } from 'lodash';
import HeaderPanel from './HeaderPanel';
import PageActions from './PageActions';
import FiltersHeader from './FiltersHeader';
import MyIssuesFilter from './MyIssuesFilter';
@@ -31,6 +30,8 @@ import IssuesList from './IssuesList';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import IssuesSourceViewer from './IssuesSourceViewer';
import BulkChangeModal from './BulkChangeModal';
import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
import {
parseQuery,
areMyIssuesSelected,
@@ -59,6 +60,7 @@ import PageFilters from '../../../components/layout/PageFilters';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { scrollToElement } from '../../../helpers/scrolling';
import type { Issue } from '../../../components/issue/types';
import '../styles.css';

type Props = {
component?: Component,
@@ -304,7 +306,7 @@ export default class App extends React.PureComponent {

fetchFirstIssues() {
this.setState({ loading: true });
this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => {
return this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => {
if (this.mounted) {
const open = getOpen(this.props.location.query);
this.setState({
@@ -321,6 +323,7 @@ export default class App extends React.PureComponent {
: undefined
});
}
return issues;
});
}

@@ -497,6 +500,14 @@ export default class App extends React.PureComponent {
this.closeBulkChange();
};

handleReloadAndOpenFirst = () => {
this.fetchFirstIssues().then(issues => {
if (issues.length > 0) {
this.openIssue(issues[0].key);
}
});
};

renderBulkChange(openIssue?: Issue) {
const { component, currentUser } = this.props;
const { bulkChange, checked, paging } = this.state;
@@ -542,6 +553,69 @@ export default class App extends React.PureComponent {
);
}

renderFacets() {
const { component, currentUser } = this.props;
const { query } = this.state;

return (
<PageFilters>
{currentUser.isLoggedIn &&
<MyIssuesFilter
myIssues={this.state.myIssues}
onMyIssuesChange={this.handleMyIssuesChange}
/>}
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
<Sidebar
component={component}
facets={this.state.facets}
myIssues={this.state.myIssues}
onFacetToggle={this.handleFacetToggle}
onFilterChange={this.handleFilterChange}
openFacets={this.state.openFacets}
query={query}
referencedComponents={this.state.referencedComponents}
referencedLanguages={this.state.referencedLanguages}
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
</PageFilters>
);
}

renderConciseIssuesList() {
const { issues, paging } = this.state;

return (
<PageFilters>
<ConciseIssuesListHeader
loading={this.state.loading}
onBackClick={this.closeIssue}
onReload={this.handleReloadAndOpenFirst}
paging={paging}
selectedIndex={this.getSelectedIndex()}
/>
<ConciseIssuesList
issues={issues}
onIssueSelect={this.openIssue}
selected={this.state.selected}
/>
{paging != null &&
paging.total > 0 &&
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
</PageFilters>
);
}

renderSide(openIssue?: Issue) {
const top = this.props.component ? 95 : 30;

return (
<PageSide top={top}>
{openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()}
</PageSide>
);
}

renderList(openIssue?: Issue) {
const { component, currentUser } = this.props;
const { issues, paging } = this.state;
@@ -575,60 +649,37 @@ export default class App extends React.PureComponent {
}

render() {
const { component, currentUser } = this.props;
const { issues, paging, query } = this.state;
const { component } = this.props;
const { issues, paging } = this.state;

const open = getOpen(this.props.location.query);
const openIssue = issues.find(issue => issue.key === open);

const selectedIndex = this.getSelectedIndex();

const top = component ? 95 : 30;

return (
<Page className="issues" id="issues-page">
<Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />

<PageSide top={top}>
<PageFilters>
{currentUser.isLoggedIn &&
<MyIssuesFilter
myIssues={this.state.myIssues}
onMyIssuesChange={this.handleMyIssuesChange}
/>}
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
<Sidebar
component={component}
facets={this.state.facets}
myIssues={this.state.myIssues}
onFacetToggle={this.handleFacetToggle}
onFilterChange={this.handleFilterChange}
openFacets={this.state.openFacets}
query={query}
referencedComponents={this.state.referencedComponents}
referencedLanguages={this.state.referencedLanguages}
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
</PageFilters>
</PageSide>
{this.renderSide(openIssue)}

<PageMain>
<HeaderPanel border={true} top={top}>
<PageMainInner>
{this.renderBulkChange(openIssue)}
{openIssue != null &&
<div className="pull-left">
<ComponentBreadcrumbs component={component} issue={openIssue} />
</div>}
<PageActions
loading={this.state.loading}
openIssue={openIssue}
paging={paging}
selectedIndex={selectedIndex}
/>
</PageMainInner>
</HeaderPanel>
<div className="issues-header-panel issues-main-header">
<div className="issues-header-panel-inner issues-main-header-inner">
<PageMainInner>
{this.renderBulkChange(openIssue)}
{openIssue != null
? <div className="pull-left">
<ComponentBreadcrumbs component={component} issue={openIssue} />
</div>
: <PageActions
loading={this.state.loading}
paging={paging}
selectedIndex={selectedIndex}
/>}
</PageMainInner>
</div>
</div>

<PageMainInner>
<div>

+ 0
- 106
server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js View File

@@ -1,106 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css, media } from 'glamor';
import { clearfix } from 'glamor/utils';
import { throttle } from 'lodash';

type Props = {|
border: boolean,
children?: React.Element<*>,
top?: number
|};

type State = {
scrolled: boolean
};

export default class HeaderPanel extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = { scrolled: this.isScrolled() };
this.handleScroll = throttle(this.handleScroll, 50);
}

componentDidMount() {
if (this.props.top != null) {
window.addEventListener('scroll', this.handleScroll);
}
}

componentWillUnmount() {
if (this.props.top != null) {
window.removeEventListener('scroll', this.handleScroll);
}
}

isScrolled = () => window.scrollY > 10;

handleScroll = () => {
this.setState({ scrolled: this.isScrolled() });
};

render() {
const commonStyles = {
height: 56,
lineHeight: '24px',
padding: '16px 20px',
boxSizing: 'border-box',
borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined,
backgroundColor: '#f3f3f3'
};

const inner = this.props.top
? <div
className={css(
commonStyles,
{
position: 'fixed',
zIndex: 30,
top: this.props.top,
left: 'calc(50vw - 360px + 1px)',
right: 0,
boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none',
transition: 'box-shadow 0.3s ease'
},
media('(max-width: 1320px)', { left: 301 })
)}>
{this.props.children}
</div>
: this.props.children;

return (
<div
className={css(clearfix(), commonStyles, {
marginTop: -20,
marginBottom: 20,
marginLeft: -20,
marginRight: -20,
'& .component-name': { lineHeight: '24px' }
})}>
{inner}
</div>
);
}
}

+ 41
- 0
server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js View File

@@ -0,0 +1,41 @@
/*
* 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 { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

type Props = {
current: ?number,
total: number
};

const IssuesCounter = (props: Props) => (
<span>
<strong>
{props.current != null && <span>{props.current + 1} / </span>}
{formatMeasure(props.total, 'INT')}
</strong>
{' '}
{translate('issues.issues')}
</span>
);

export default IssuesCounter;

+ 4
- 13
server/sonar-web/src/main/js/apps/issues/components/PageActions.js View File

@@ -20,13 +20,12 @@
// @flow
import React from 'react';
import { css } from 'glamor';
import IssuesCounter from './IssuesCounter';
import type { Paging } from '../utils';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

type Props = {|
loading: boolean,
openIssue: ?{},
paging: ?Paging,
selectedIndex: ?number
|};
@@ -53,23 +52,15 @@ export default class PageActions extends React.PureComponent {
}

render() {
const { openIssue, paging, selectedIndex } = this.props;
const { paging, selectedIndex } = this.props;

return (
<div className={css({ float: 'right' })}>
{openIssue == null && this.renderShortcuts()}
{this.renderShortcuts()}

<div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
{this.props.loading && <i className="spinner spacer-right" />}
{paging != null &&
<span>
<strong>
{selectedIndex != null && <span>{selectedIndex + 1} / </span>}
{formatMeasure(paging.total, 'INT')}
</strong>
{' '}
{translate('issues.issues')}
</span>}
{paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
</div>
</div>
);

+ 51
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js View File

@@ -0,0 +1,51 @@
/*
* 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';

type Props = {|
className?: string,
onClick: () => void
|};

/* eslint-disable max-len */
const icon = (
<svg width="21" height="24" viewBox="0 0 21 24">
<path d="M3.845 12.9992l5.993 5.993.052.056c.049.061.093.122.129.191.082.159.121.339.111.518-.006.102-.028.203-.064.298-.149.39-.537.652-.954.644-.102-.002-.204-.019-.301-.052-.148-.05-.273-.135-.387-.241l-8.407-8.407 8.407-8.407.056-.052c.061-.048.121-.092.19-.128.116-.06.237-.091.366-.108.076-.004.075-.004.153-.003.155.015.3.052.437.129.088.051.169.115.239.19.246.266.33.656.214.999-.051.149-.135.273-.241.387l-5.983 5.984c5.287-.044 10.577-.206 15.859.013.073.009.091.009.163.027.187.047.359.15.49.292.075.081.136.175.18.276.044.101.072.209.081.319.032.391-.175.775-.521.962-.097.052-.202.089-.311.107-.073.012-.091.01-.165.013H3.845z" />
</svg>
);
/* eslint-enable max-len */

export default function BackButton(props: Props) {
const handleClick = (event: Event) => {
event.preventDefault();
props.onClick();
};

return (
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
);
}

+ 49
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js View File

@@ -0,0 +1,49 @@
/*
* 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 ConciseIssueBox from './ConciseIssueBox';
import ConciseIssueComponent from './ConciseIssueComponent';
import type { Issue } from '../../../components/issue/types';

type Props = {|
innerRef: HTMLElement => void,
issue: Issue,
onSelect: string => void,
previousIssue: ?Issue,
selected: boolean
|};

export default class ConciseIssue extends React.PureComponent {
props: Props;

render() {
const { issue, previousIssue, selected } = this.props;

const displayComponent = previousIssue == null || previousIssue.component !== issue.component;

return (
<div ref={this.props.innerRef}>
{displayComponent && <ConciseIssueComponent path={issue.componentLongName} />}
<ConciseIssueBox issue={issue} onClick={this.props.onSelect} selected={selected} />
</div>
);
}
}

+ 54
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js View File

@@ -0,0 +1,54 @@
/*
* 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 ConciseIssueLocations from './ConciseIssueLocations';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import TypeHelper from '../../../components/shared/TypeHelper';
import type { Issue } from '../../../components/issue/types';

type Props = {|
issue: Issue,
onClick: string => void,
selected: boolean
|};

export default function ConciseIssueBox(props: Props) {
const { issue, selected } = props;

const handleClick = (event: Event) => {
event.preventDefault();
props.onClick(issue.key);
};

const clickAttributes = selected ? {} : { onClick: handleClick, role: 'listitem', tabIndex: 0 };

return (
<div className={classNames('concise-issue-box', { selected })} {...clickAttributes}>
<div className="concise-issue-box-message">{issue.message}</div>
<div className="concise-issue-box-attributes">
<TypeHelper type={issue.type} />
<SeverityHelper className="big-spacer-left" severity={issue.severity} />
<ConciseIssueLocations flows={issue.flows} />
</div>
</div>
);
}

+ 34
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js View File

@@ -0,0 +1,34 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { collapsePath } from '../../../helpers/path';

type Props = {
path: string
};

const ConciseIssueComponent = (props: Props) => (
<div className="concise-issue-component note text-ellipsis">
{collapsePath(props.path, 20)}
</div>
);

export default ConciseIssueComponent;

+ 42
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js View File

@@ -0,0 +1,42 @@
/*
* 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 Tooltip from '../../../components/controls/Tooltip';
import { translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

type Props = {|
count: number
|};

export default function ConciseIssueLocationBadge(props: Props) {
return (
<Tooltip
overlay={translateWithParameters(
'issue.this_issue_involves_x_code_locations',
formatMeasure(props.count)
)}>
<div className="concise-issue-location-badge">
{'+'}{props.count}
</div>
</Tooltip>
);
}

+ 57
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js View File

@@ -0,0 +1,57 @@
/*
* 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 ConciseIssueLocationBadge from './ConciseIssueLocationBadge';
import type { FlowLocation } from '../../../components/issue/types';

type Props = {|
flows: Array<{
locations?: Array<FlowLocation>
}>
|};

export default class ConciseIssueLocations extends React.PureComponent {
props: Props;

render() {
const { flows } = this.props;

const secondaryLocations = flows.filter(
flow => flow.locations != null && flow.locations.length === 1
).length;

const realFlows = flows.filter(flow => flow.locations != null && flow.locations.length > 1);

return (
<div className="pull-right">
{secondaryLocations > 0 && <ConciseIssueLocationBadge count={secondaryLocations} />}

{realFlows.map((flow, index) => (
<ConciseIssueLocationBadge
// $FlowFixMe locations are not null
count={flow.locations.length}
key={index}
/>
))}
</div>
);
}
}

+ 84
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js View File

@@ -0,0 +1,84 @@
/*
* 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 ConciseIssue from './ConciseIssue';
import { scrollToElement } from '../../../helpers/scrolling';
import type { Issue } from '../../../components/issue/types';

type Props = {|
issues: Array<Issue>,
onIssueSelect: string => void,
selected?: string
|};

export default class ConciseIssuesList extends React.PureComponent {
nodes: { [string]: HTMLElement };
props: Props;

constructor(props: Props) {
super(props);
this.nodes = {};
}

componentDidMount() {
if (this.props.selected) {
this.ensureSelectedVisible();
}
}

componentDidUpdate(prevProps: Props) {
if (this.props.selected && prevProps.selected !== this.props.selected) {
this.ensureSelectedVisible();
}
}

ensureSelectedVisible() {
const { selected } = this.props;
if (selected) {
const scrollableElement = document.querySelector('.layout-page-side');
const element = this.nodes[selected];
if (element && scrollableElement) {
scrollToElement(element, 150, 100, scrollableElement);
}
}
}

innerRef = (issue: string) => (node: HTMLElement) => {
this.nodes[issue] = node;
};

render() {
return (
<div>
{this.props.issues.map((issue, index) => (
<ConciseIssue
key={issue.key}
innerRef={this.innerRef(issue.key)}
issue={issue}
onSelect={this.props.onIssueSelect}
previousIssue={index > 0 ? this.props.issues[index - 1] : null}
selected={issue.key === this.props.selected}
/>
))}
</div>
);
}
}

+ 49
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js View File

@@ -0,0 +1,49 @@
/*
* 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 BackButton from './BackButton';
import ReloadButton from './ReloadButton';
import IssuesCounter from '../components/IssuesCounter';
import type { Paging } from '../utils';

type Props = {|
loading: boolean,
onBackClick: () => void,
onReload: () => void,
paging?: Paging,
selectedIndex: ?number
|};

export default function ConciseIssuesListHeader(props: Props) {
const { paging, selectedIndex } = props;

return (
<header className="issues-header-panel concise-issues-list-header">
<div className="issues-header-panel-inner concise-issues-list-header-inner">
<BackButton className="pull-left" onClick={props.onBackClick} />
{props.loading
? <i className="spinner pull-right" />
: <ReloadButton className="pull-right" onClick={props.onReload} />}
{paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
</div>
</header>
);
}

+ 51
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js View File

@@ -0,0 +1,51 @@
/*
* 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';

type Props = {|
className?: string,
onClick: () => void
|};

/* eslint-disable max-len */
const icon = (
<svg width="18" height="24" viewBox="0 0 18 24">
<path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
</svg>
);
/* eslint-enable max-len */

export default function ReloadButton(props: Props) {
const handleClick = (event: Event) => {
event.preventDefault();
props.onClick();
};

return (
<a
className={classNames('concise-issues-list-header-button', props.className)}
href="#"
onClick={handleClick}>
{icon}
</a>
);
}

+ 41
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js View File

@@ -0,0 +1,41 @@
/*
* 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 ConciseIssue from '../ConciseIssue';

it('should render', () => {
expect(
shallow(<ConciseIssue issue={{}} onSelect={jest.fn()} selected={false} />)
).toMatchSnapshot();
});

it('should not render component', () => {
expect(
shallow(
<ConciseIssue
issue={{ component: 'foo' }}
onSelect={jest.fn()}
previousIssue={{ component: 'foo' }}
selected={false}
/>
).find('ConciseIssueComponent')
).toHaveLength(0);
});

+ 29
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js View File

@@ -0,0 +1,29 @@
/*
* 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 { shallow } from 'enzyme';
import ConciseIssueComponent from '../ConciseIssueComponent';

it('should render', () => {
expect(
shallow(<ConciseIssueComponent path="src/app/folder/sub-folder/long-folder/name/app.js" />)
).toMatchSnapshot();
});

+ 27
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js View File

@@ -0,0 +1,27 @@
/*
* 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 { shallow } from 'enzyme';
import ConciseIssueLocationBadge from '../ConciseIssueLocationBadge';

it('should render', () => {
expect(shallow(<ConciseIssueLocationBadge count={7} />)).toMatchSnapshot();
});

+ 50
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js View File

@@ -0,0 +1,50 @@
/*
* 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 { shallow } from 'enzyme';
import ConciseIssueLocations from '../ConciseIssueLocations';

const textRange = { startLine: 1, startOffset: 1, endLine: 1, endOffset: 1 };

it('should render only secondary locations', () => {
const flows = [
{ locations: [{ msg: '', textRange }] },
{ locations: [{ msg: '', textRange }] },
{ locations: [{ msg: '', textRange }] }
];
expect(shallow(<ConciseIssueLocations flows={flows} />)).toMatchSnapshot();
});

it('should render one flow', () => {
const flows = [
{ locations: [{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }] }
];
expect(shallow(<ConciseIssueLocations flows={flows} />)).toMatchSnapshot();
});

it('should render several flows', () => {
const flows = [
{ locations: [{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }] },
{ locations: [{ msg: '', textRange }, { msg: '', textRange }] },
{ locations: [{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }] }
];
expect(shallow(<ConciseIssueLocations flows={flows} />)).toMatchSnapshot();
});

+ 27
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js View File

@@ -0,0 +1,27 @@
/*
* 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 ConciseIssuesList from '../ConciseIssuesList';

it('should render', () => {
const issues = [{ key: 'foo' }, { key: 'bar' }];
expect(shallow(<ConciseIssuesList issues={issues} />)).toMatchSnapshot();
});

+ 9
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap View File

@@ -0,0 +1,9 @@
exports[`test should render 1`] = `
<div>
<ConciseIssueComponent />
<ConciseIssueBox
issue={Object {}}
onClick={[Function]}
selected={false} />
</div>
`;

+ 6
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap View File

@@ -0,0 +1,6 @@
exports[`test should render 1`] = `
<div
className="concise-issue-component note text-ellipsis">
src/.../long-folder/name/app.js
</div>
`;

+ 11
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap View File

@@ -0,0 +1,11 @@
exports[`test should render 1`] = `
<Tooltip
overlay="issue.this_issue_involves_x_code_locations.7"
placement="bottom">
<div
className="concise-issue-location-badge">
+
7
</div>
</Tooltip>
`;

+ 27
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap View File

@@ -0,0 +1,27 @@
exports[`test should render one flow 1`] = `
<div
className="pull-right">
<ConciseIssueLocationBadge
count={3} />
</div>
`;

exports[`test should render only secondary locations 1`] = `
<div
className="pull-right">
<ConciseIssueLocationBadge
count={3} />
</div>
`;

exports[`test should render several flows 1`] = `
<div
className="pull-right">
<ConciseIssueLocationBadge
count={3} />
<ConciseIssueLocationBadge
count={2} />
<ConciseIssueLocationBadge
count={3} />
</div>
`;

+ 26
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap View File

@@ -0,0 +1,26 @@
exports[`test should render 1`] = `
<div>
<ConciseIssue
innerRef={[Function]}
issue={
Object {
"key": "foo",
}
}
previousIssue={null}
selected={false} />
<ConciseIssue
innerRef={[Function]}
issue={
Object {
"key": "bar",
}
}
previousIssue={
Object {
"key": "foo",
}
}
selected={false} />
</div>
`;

+ 126
- 0
server/sonar-web/src/main/js/apps/issues/styles.css View File

@@ -0,0 +1,126 @@
.issues-header-panel,
.issues-header-panel-inner {
height: 56px;
box-sizing: border-box;
}

.issues-header-panel {
margin-top: -20px;
}

.issues-header-panel-inner {
position: fixed;
z-index: 30;
line-height: 24px;
padding-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e6e6e6;
background-color: #f3f3f3;
}

.issues-main-header {
margin-bottom: 20px;
}

.issues-main-header .component-name {
line-height: 24px;
}

.issues-main-header-inner {
left: calc(50vw - 360px + 1px);
right: 0;
padding-left: 20px;
padding-right: 20px;
}

@media (max-width: 1320px) {
.issues-main-header-inner {
left: 301px;
}
}

.concise-issues-list-header,
.concise-issues-list-header-inner {
}

.concise-issues-list-header {
}

.concise-issues-list-header-inner {
width: 260px;
text-align: center;
}

.concise-issues-list-header .spinner {
margin-top: 4px;
margin-left: 1px;
margin-right: 1px;
}

.concise-issues-list-header-button {
border: none;
}

.concise-issues-list-header-button path {
fill: #777;
transition: fill 0.3s ease;
}

.concise-issues-list-header-button:hover path {
fill: #4b9fd5;
}

.concise-issue-component {
margin-top: 16px;
margin-bottom: 4px;
padding-left: 8px;
padding-right: 8px;
}

.concise-issue-box {
position: relative;
z-index: 1;
margin-bottom: 4px;
padding: 8px;
border: 1px solid #e6e6e6;
background-color: #fff;
cursor: pointer;
transition: background-color 0.3s ease, border-color 0.3s ease;
}

.concise-issue-box:hover,
.concise-issue-box:focus {
background-color: #ffeaea;
outline: none
}

.concise-issue-box.selected {
z-index: 2;
border-color: #dd4040;
background-color: #ffeaea;
cursor: default
}

.concise-issue-box-message {
font-weight: bold;
}

.concise-issue-box-attributes {
margin-top: 8px;
line-height: 16px;
font-size: 12px;
}

.concise-issue-location-badge {
display: inline-block;
padding-left: 4px;
padding-right: 4px;
border-radius: 2px;
background-color: #ccc;
color: #fff;
transition: background-color 0.3s ease;
}

.concise-issue-box.selected .concise-issue-location-badge {
background-color: #d18582;
}

+ 2
- 2
server/sonar-web/src/main/js/components/layout/PageSide.js View File

@@ -36,7 +36,6 @@ const width = css(
const sideStyles = css(width, {
flexGrow: 0,
flexShrink: 0,
borderRight: '1px solid #e6e6e6',
backgroundColor: '#f3f3f3'
});

@@ -46,6 +45,7 @@ const sideStickyStyles = css(width, {
top: 0,
bottom: 0,
left: 0,
borderRight: '1px solid #e6e6e6',
overflowY: 'auto',
overflowX: 'hidden',
backgroundColor: '#f3f3f3'
@@ -63,7 +63,7 @@ const sideInnerStyles = css(
export default function PageSide(props: Props) {
return (
<div className={sideStyles}>
<div className={sideStickyStyles} style={{ top: props.top || 30 }}>
<div className={`layout-page-side ${sideStickyStyles}`} style={{ top: props.top || 30 }}>
<div className={sideInnerStyles}>
{props.children}
</div>

+ 36
- 0
server/sonar-web/src/main/js/components/shared/TypeHelper.js View File

@@ -0,0 +1,36 @@
/*
* 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 IssueTypeIcon from '../ui/IssueTypeIcon';
import { translate } from '../../helpers/l10n';

type Props = {
type: string
};

const TypeHelper = (props: Props) => (
<span>
<IssueTypeIcon className="little-spacer-right" query={props.type} />
{translate('issue.type', props.type)}
</span>
);

export default TypeHelper;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -675,6 +675,7 @@ issue.effort=Effort:
issue.x_effort={0} effort
issue.creation_date=Created
issue.filter_similar_issues=Filter Similar Issues
issue.this_issue_involves_x_code_locations=This issue involved {0} code locations
issues.return_to_list=Return to List
issues.issues_limit_reached=For usability reasons, only the {0} issues are displayed.
issues.bulk_change=All Issues ({0})

Loading…
Cancel
Save