aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <stas-vilchik@users.noreply.github.com>2017-04-20 16:59:36 +0200
committerGitHub <noreply@github.com>2017-04-20 16:59:36 +0200
commitb27171e2644049cc08d9886183abb624ab2955ea (patch)
tree9037545fc3e3a6fbfec1a0994600cf116460d01c /server/sonar-web/src/main/js
parent7891bd3a71b0aec2d87a413f61e3c9859925717e (diff)
downloadsonarqube-b27171e2644049cc08d9886183abb624ab2955ea.tar.gz
sonarqube-b27171e2644049cc08d9886183abb624ab2955ea.zip
SONAR-9065 Display concise issues list when browsing code (#1953)
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js139
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js106
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js41
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/PageActions.js17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js49
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js54
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js34
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js42
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js57
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js84
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js49
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js41
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js29
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js50
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css126
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageSide.js4
-rw-r--r--server/sonar-web/src/main/js/components/shared/TypeHelper.js36
26 files changed, 1028 insertions, 165 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
index efdfd182789..ebac6e45d41 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
deleted file mode 100644
index 2d8530dab51..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js
new file mode 100644
index 00000000000..0c6814735e1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
index d1b1d6cfdd8..dcefa0a7d1b 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js
new file mode 100644
index 00000000000..f621e0a83ea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js
new file mode 100644
index 00000000000..94c1d531827
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssue.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
new file mode 100644
index 00000000000..d0f09aa95d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js
new file mode 100644
index 00000000000..17337603555
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueComponent.js
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js
new file mode 100644
index 00000000000..fe8cbb7eecc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationBadge.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js
new file mode 100644
index 00000000000..aedc4c5a261
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
new file mode 100644
index 00000000000..055d1aba2e7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
new file mode 100644
index 00000000000..2be4b44732e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js
new file mode 100644
index 00000000000..0034fad7d48
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js
new file mode 100644
index 00000000000..3de6cb4120b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssue-test.js
@@ -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);
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js
new file mode 100644
index 00000000000..f8db79b215b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueComponent-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js
new file mode 100644
index 00000000000..ff27fa03501
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationBadge-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js
new file mode 100644
index 00000000000..400c17089a4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js
new file mode 100644
index 00000000000..f57f5abe469
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssuesList-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap
new file mode 100644
index 00000000000..a85d767247e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssue-test.js.snap
@@ -0,0 +1,9 @@
+exports[`test should render 1`] = `
+<div>
+ <ConciseIssueComponent />
+ <ConciseIssueBox
+ issue={Object {}}
+ onClick={[Function]}
+ selected={false} />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap
new file mode 100644
index 00000000000..5565dc617c9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueComponent-test.js.snap
@@ -0,0 +1,6 @@
+exports[`test should render 1`] = `
+<div
+ className="concise-issue-component note text-ellipsis">
+ src/.../long-folder/name/app.js
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap
new file mode 100644
index 00000000000..654a5c5e517
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationBadge-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap
new file mode 100644
index 00000000000..cbd406bb647
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocations-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap
new file mode 100644
index 00000000000..1db833d56a7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssuesList-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
new file mode 100644
index 00000000000..260e667fc5b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -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;
+} \ No newline at end of file
diff --git a/server/sonar-web/src/main/js/components/layout/PageSide.js b/server/sonar-web/src/main/js/components/layout/PageSide.js
index 24d810075ca..a647d83c0c1 100644
--- a/server/sonar-web/src/main/js/components/layout/PageSide.js
+++ b/server/sonar-web/src/main/js/components/layout/PageSide.js
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/shared/TypeHelper.js b/server/sonar-web/src/main/js/components/shared/TypeHelper.js
new file mode 100644
index 00000000000..f984e4f4e66
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/TypeHelper.js
@@ -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;