@@ -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> |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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; |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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; |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -0,0 +1,9 @@ | |||
exports[`test should render 1`] = ` | |||
<div> | |||
<ConciseIssueComponent /> | |||
<ConciseIssueBox | |||
issue={Object {}} | |||
onClick={[Function]} | |||
selected={false} /> | |||
</div> | |||
`; |
@@ -0,0 +1,6 @@ | |||
exports[`test should render 1`] = ` | |||
<div | |||
className="concise-issue-component note text-ellipsis"> | |||
src/.../long-folder/name/app.js | |||
</div> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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; | |||
} |
@@ -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> |
@@ -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; |
@@ -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}) |