@@ -17,12 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import App from './App'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { getJSON } from '../helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export function getBranches(project: string): Promise<any> { | |||
return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); | |||
} | |||
export default connect(mapStateToProps)(App); | |||
export function getBranch(project: string, branch: string): Promise<any> { | |||
return getJSON('/api/project_branches/show', { component: project, branch }).then( | |||
r => r.branch, | |||
throwGlobalError | |||
); | |||
} |
@@ -113,8 +113,12 @@ export function getComponentLeaves( | |||
return getComponentTree('leaves', componentKey, metrics, additional); | |||
} | |||
export function getComponent(componentKey: string, metrics: string[] = []): Promise<any> { | |||
const data = { componentKey, metricKeys: metrics.join(',') }; | |||
export function getComponent( | |||
componentKey: string, | |||
metrics: string[] = [], | |||
branch?: string | |||
): Promise<any> { | |||
const data = { branch, componentKey, metricKeys: metrics.join(',') }; | |||
return getJSON('/api/measures/component', data).then(r => r.component); | |||
} | |||
@@ -122,23 +126,23 @@ export function getTree(component: string, options: RequestData = {}): Promise<a | |||
return getJSON('/api/components/tree', { ...options, component }); | |||
} | |||
export function getComponentShow(component: string): Promise<any> { | |||
return getJSON('/api/components/show', { component }); | |||
export function getComponentShow(component: string, branch?: string): Promise<any> { | |||
return getJSON('/api/components/show', { component, branch }); | |||
} | |||
export function getParents(component: string): Promise<any> { | |||
return getComponentShow(component).then(r => r.ancestors); | |||
} | |||
export function getBreadcrumbs(component: string): Promise<any> { | |||
return getComponentShow(component).then(r => { | |||
export function getBreadcrumbs(component: string, branch?: string): Promise<any> { | |||
return getComponentShow(component, branch).then(r => { | |||
const reversedAncestors = [...r.ancestors].reverse(); | |||
return [...reversedAncestors, r.component]; | |||
}); | |||
} | |||
export function getComponentData(component: string): Promise<any> { | |||
return getComponentShow(component).then(r => r.component); | |||
export function getComponentData(component: string, branch?: string): Promise<any> { | |||
return getComponentShow(component, branch).then(r => r.component); | |||
} | |||
export function getMyProjects(data: RequestData): Promise<any> { | |||
@@ -219,12 +223,17 @@ export function getSuggestions( | |||
return getJSON('/api/components/suggestions', data); | |||
} | |||
export function getComponentForSourceViewer(component: string): Promise<any> { | |||
return getJSON('/api/components/app', { component }); | |||
export function getComponentForSourceViewer(component: string, branch?: string): Promise<any> { | |||
return getJSON('/api/components/app', { component, branch }); | |||
} | |||
export function getSources(component: string, from?: number, to?: number): Promise<any> { | |||
const data: RequestData = { key: component }; | |||
export function getSources( | |||
component: string, | |||
from?: number, | |||
to?: number, | |||
branch?: string | |||
): Promise<any> { | |||
const data: RequestData = { key: component, branch }; | |||
if (from) { | |||
Object.assign(data, { from }); | |||
} |
@@ -18,15 +18,16 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON } from '../helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getGlobalNavigation(): Promise<any> { | |||
return getJSON('/api/navigation/global'); | |||
} | |||
export function getComponentNavigation(componentKey: string): Promise<any> { | |||
return getJSON('/api/navigation/component', { componentKey }); | |||
export function getComponentNavigation(componentKey: string, branch?: string): Promise<any> { | |||
return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError); | |||
} | |||
export function getSettingsNavigation(): Promise<any> { | |||
return getJSON('/api/navigation/settings'); | |||
return getJSON('/api/navigation/settings').catch(throwGlobalError); | |||
} |
@@ -18,14 +18,12 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getComponent } from '../../store/rootReducer'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
class ProjectAdminContainer extends React.PureComponent { | |||
export default class ProjectAdminContainer extends React.PureComponent { | |||
/*:: | |||
props: { | |||
project: { | |||
component: { | |||
configuration?: { | |||
showSettings: boolean | |||
} | |||
@@ -42,7 +40,7 @@ class ProjectAdminContainer extends React.PureComponent { | |||
} | |||
isProjectAdmin() { | |||
const { configuration } = this.props.project; | |||
const { configuration } = this.props.component; | |||
return configuration != null && configuration.showSettings; | |||
} | |||
@@ -57,12 +55,8 @@ class ProjectAdminContainer extends React.PureComponent { | |||
return null; | |||
} | |||
return this.props.children; | |||
return React.cloneElement(this.props.children, { | |||
component: this.props.component | |||
}); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(ProjectAdminContainer); |
@@ -1,104 +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 { connect } from 'react-redux'; | |||
import ComponentNav from './nav/component/ComponentNav'; | |||
import { fetchProject } from '../../store/rootActions'; | |||
import { getComponent } from '../../store/rootReducer'; | |||
import { addGlobalErrorMessage } from '../../store/globalMessages/duck'; | |||
import { receiveComponents } from '../../store/components/actions'; | |||
import { parseError } from '../../apps/code/utils'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
class ProjectContainer extends React.PureComponent { | |||
/*:: | |||
props: { | |||
addGlobalErrorMessage: (message: string) => void, | |||
children?: React.Element<*>, | |||
location: { | |||
query: { id: string } | |||
}, | |||
project?: { | |||
configuration: {}, | |||
name: string, | |||
qualifier: string | |||
}, | |||
fetchProject: string => Promise<*>, | |||
receiveComponents: (Array<*>) => void | |||
}; | |||
*/ | |||
componentDidMount() { | |||
this.fetchProject(); | |||
} | |||
componentDidUpdate(prevProps) { | |||
if (prevProps.location.query.id !== this.props.location.query.id) { | |||
this.fetchProject(); | |||
} | |||
} | |||
fetchProject() { | |||
this.props.fetchProject(this.props.location.query.id).catch(e => { | |||
if (e.response && e.response.status === 403) { | |||
handleRequiredAuthorization(); | |||
} else { | |||
parseError(e).then(message => this.props.addGlobalErrorMessage(message)); | |||
} | |||
}); | |||
} | |||
handleProjectChange = (changes /*: {} */) => { | |||
this.props.receiveComponents([{ ...this.props.project, ...changes }]); | |||
}; | |||
render() { | |||
const { project } = this.props; | |||
// check `breadcrumbs` to be sure that /api/navigation/component has been already called | |||
if (!project || project.breadcrumbs == null) { | |||
return null; | |||
} | |||
const isFile = ['FIL', 'UTS'].includes(project.qualifier); | |||
const configuration = project.configuration || {}; | |||
return ( | |||
<div> | |||
{!isFile && | |||
<ComponentNav component={project} conf={configuration} location={this.props.location} />} | |||
{/* $FlowFixMe */} | |||
{React.cloneElement(this.props.children, { | |||
component: project, | |||
onComponentChange: this.handleProjectChange | |||
})} | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { addGlobalErrorMessage, fetchProject, receiveComponents }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectContainer); |
@@ -0,0 +1,143 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import ComponentNav from './nav/component/ComponentNav'; | |||
import { Branch, Component } from '../types'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
import { getBranch } from '../../api/branches'; | |||
import { getComponentData } from '../../api/components'; | |||
import { getComponentNavigation } from '../../api/nav'; | |||
import { MAIN_BRANCH } from '../../helpers/branches'; | |||
interface Props { | |||
children: any; | |||
location: { | |||
query: { branch?: string; id: string }; | |||
}; | |||
} | |||
interface State { | |||
branch: Branch | null; | |||
loading: boolean; | |||
component: Component | null; | |||
} | |||
export default class ProjectContainer extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { branch: null, loading: true, component: null }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchProject(); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
// if the current branch has been changed, reset `branch` in state | |||
// it prevents unwanted redirect in `overview/App#componentDidMount` | |||
if (nextProps.location.query.branch !== this.props.location.query.branch) { | |||
this.setState({ branch: null }); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if ( | |||
prevProps.location.query.id !== this.props.location.query.id || | |||
prevProps.location.query.branch !== this.props.location.query.branch | |||
) { | |||
this.fetchProject(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
addQualifier = (component: Component) => ({ | |||
...component, | |||
qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier | |||
}); | |||
fetchProject() { | |||
const { branch, id } = this.props.location.query; | |||
this.setState({ loading: true }); | |||
Promise.all([ | |||
getComponentNavigation(id), | |||
getComponentData(id, branch), | |||
branch && getBranch(id, branch) | |||
]).then( | |||
([nav, data, branch]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
branch: branch || MAIN_BRANCH, | |||
component: this.addQualifier({ ...nav, ...data }) | |||
}); | |||
} | |||
}, | |||
error => { | |||
if (this.mounted) { | |||
if (error.response && error.response.status === 403) { | |||
handleRequiredAuthorization(); | |||
} else { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
} | |||
); | |||
} | |||
handleProjectChange = (changes: {}) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ component: { ...state.component, ...changes } })); | |||
} | |||
}; | |||
render() { | |||
const { branch, component } = this.state; | |||
if (!component || !branch) { | |||
return null; | |||
} | |||
const isFile = ['FIL', 'UTS'].includes(component.qualifier); | |||
const configuration = component.configuration || {}; | |||
return ( | |||
<div> | |||
{!isFile && | |||
<ComponentNav | |||
branch={branch} | |||
component={component} | |||
conf={configuration} | |||
location={this.props.location} | |||
/>} | |||
{React.cloneElement(this.props.children, { | |||
branch, | |||
component: component, | |||
onComponentChange: this.handleProjectChange | |||
})} | |||
</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. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectContainer from '../ProjectContainer'; | |||
it('changes component', () => { | |||
const Inner = () => <div />; | |||
const wrapper = shallow( | |||
<ProjectContainer location={{ query: { id: 'foo' } }}> | |||
<Inner /> | |||
</ProjectContainer> | |||
); | |||
(wrapper.instance() as ProjectContainer).mounted = true; | |||
wrapper.setState({ | |||
branch: { isMain: true }, | |||
component: { qualifier: 'TRK', visibility: 'public' }, | |||
loading: false | |||
}); | |||
(wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' }); | |||
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); | |||
}); |
@@ -17,8 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
export default class ExtensionNotFound extends React.PureComponent { |
@@ -22,7 +22,6 @@ import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; | |||
/*:: | |||
@@ -51,10 +50,6 @@ function ProjectAdminPageExtension(props /*: Props */) { | |||
: <ExtensionNotFound />; | |||
} | |||
const mapStateToProps = (state, ownProps /*: Props */) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { onFail: addGlobalErrorMessage }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectAdminPageExtension); | |||
export default connect(null, mapDispatchToProps)(ProjectAdminPageExtension); |
@@ -17,40 +17,27 @@ | |||
* 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 { connect } from 'react-redux'; | |||
import * as React from 'react'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; | |||
import { Component } from '../../types'; | |||
/*:: | |||
type Props = { | |||
component: { | |||
extensions: Array<{ key: string }> | |||
}, | |||
location: { query: { id: string } }, | |||
interface Props { | |||
component: Component; | |||
location: { query: { id: string } }; | |||
params: { | |||
extensionKey: string, | |||
pluginKey: string | |||
} | |||
}; | |||
*/ | |||
extensionKey: string; | |||
pluginKey: string; | |||
}; | |||
} | |||
function ProjectPageExtension(props /*: Props */) { | |||
export default function ProjectPageExtension(props: Props) { | |||
const { extensionKey, pluginKey } = props.params; | |||
const { component } = props; | |||
const extension = component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`); | |||
const extension = | |||
component.extensions && | |||
component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`); | |||
return extension | |||
? <Extension extension={extension} options={{ component }} /> | |||
: <ExtensionNotFound />; | |||
} | |||
const mapStateToProps = (state, ownProps /*: Props */) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { onFail: addGlobalErrorMessage }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectPageExtension); |
@@ -0,0 +1,18 @@ | |||
.branch-status { | |||
} | |||
.branch-status-indicator { | |||
display: block; | |||
width: 8px; | |||
height: 8px; | |||
border-radius: 8px; | |||
margin: 4px 0; | |||
} | |||
.branch-status-indicator.is-failed { | |||
background-color: #d4333f; | |||
} | |||
.branch-status-indicator.is-passed { | |||
background-color: #00aa00; | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Branch } from '../../../types'; | |||
import BugIcon from '../../../../components/icons-components/BugIcon'; | |||
import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon'; | |||
import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import './BranchStatus.css'; | |||
interface Props { | |||
branch: Branch; | |||
concise?: boolean; | |||
} | |||
export default function BranchStatus({ branch, concise = false }: Props) { | |||
// TODO handle long-living branches | |||
if (!isShortLivingBranch(branch)) { | |||
return null; | |||
} | |||
const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; | |||
return ( | |||
<ul className="list-inline branch-status"> | |||
<li> | |||
<i | |||
className={classNames('branch-status-indicator', { | |||
'is-failed': totalIssues > 0, | |||
'is-passed': totalIssues === 0 | |||
})} | |||
/> | |||
</li> | |||
{concise && | |||
<li> | |||
{totalIssues} | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.bugs} | |||
<BugIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.vulnerabilities} | |||
<VulnerabilityIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.codeSmells} | |||
<CodeSmellIcon className="little-spacer-left" /> | |||
</li>} | |||
</ul> | |||
); | |||
} |
@@ -9,3 +9,20 @@ | |||
padding-top: 5px; | |||
box-sizing: border-box; | |||
} | |||
.navbar-context-branches { | |||
float: left; | |||
padding: 8px 0 6px; | |||
margin-left: 16px; | |||
line-height: 16px; | |||
} | |||
.navbar-context-meta-branch { | |||
margin-top: 20px; | |||
line-height: 16px; | |||
} | |||
.navbar-context-meta-branch-menu-item { | |||
display: flex !important; | |||
justify-content: space-between; | |||
} |
@@ -17,21 +17,40 @@ | |||
* 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 * as React from 'react'; | |||
import ComponentNavFavorite from './ComponentNavFavorite'; | |||
import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs'; | |||
import ComponentNavMeta from './ComponentNavMeta'; | |||
import ComponentNavMenu from './ComponentNavMenu'; | |||
import ComponentNavBranch from './ComponentNavBranch'; | |||
import RecentHistory from '../../RecentHistory'; | |||
import { Branch, Component, ComponentConfiguration } from '../../../types'; | |||
import ContextNavBar from '../../../../components/nav/ContextNavBar'; | |||
import { getTasksForComponent } from '../../../../api/ce'; | |||
import { STATUSES } from '../../../../apps/background-tasks/constants'; | |||
import './ComponentNav.css'; | |||
export default class ComponentNav extends React.PureComponent { | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
location: {}; | |||
} | |||
interface State { | |||
incremental?: boolean; | |||
isFailed?: boolean; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
} | |||
export default class ComponentNav extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = {}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.loadStatus(); | |||
this.populateRecentHistory(); | |||
} | |||
@@ -41,11 +60,11 @@ export default class ComponentNav extends React.PureComponent { | |||
} | |||
loadStatus = () => { | |||
getTasksForComponent(this.props.component.key).then(r => { | |||
getTasksForComponent(this.props.component.key).then((r: any) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
isPending: r.queue.some(task => task.status === STATUSES.PENDING), | |||
isInProgress: r.queue.some(task => task.status === STATUSES.IN_PROGRESS), | |||
isPending: r.queue.some((task: any) => task.status === STATUSES.PENDING), | |||
isInProgress: r.queue.some((task: any) => task.status === STATUSES.IN_PROGRESS), | |||
isFailed: r.current && r.current.status === STATUSES.FAILED, | |||
incremental: r.current && r.current.incremental | |||
}); | |||
@@ -79,17 +98,19 @@ export default class ComponentNav extends React.PureComponent { | |||
breadcrumbs={this.props.component.breadcrumbs} | |||
/> | |||
<ComponentNavBranch branch={this.props.branch} project={this.props.component} /> | |||
<ComponentNavMeta | |||
{...this.props} | |||
{...this.state} | |||
version={this.props.component.version} | |||
analysisDate={this.props.component.analysisDate} | |||
branch={this.props.branch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
incremental={this.state.incremental} | |||
/> | |||
<ComponentNavMenu | |||
branch={this.props.branch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
location={this.props.location} | |||
/> | |||
</ContextNavBar> | |||
); |
@@ -0,0 +1,84 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { getBranchDisplayName } from '../../../../helpers/branches'; | |||
interface Props { | |||
branch: Branch; | |||
project: Component; | |||
} | |||
interface State { | |||
open: boolean; | |||
} | |||
export default class ComponentNavBranch extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { open: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.project !== this.props.project || nextProps.branch !== this.props.branch) { | |||
this.setState({ open: false }); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleClick = (event: React.SyntheticEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.setState({ open: true }); | |||
}; | |||
closeDropdown = () => { | |||
if (this.mounted) { | |||
this.setState({ open: false }); | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<div className={classNames('navbar-context-branches', 'dropdown', { open: this.state.open })}> | |||
<a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}> | |||
<BranchIcon className="little-spacer-right" /> | |||
{getBranchDisplayName(this.props.branch)} | |||
<i className="icon-dropdown little-spacer-left" /> | |||
</a> | |||
{this.state.open && | |||
<ComponentNavBranchesMenu | |||
branch={this.props.branch} | |||
onClose={this.closeDropdown} | |||
project={this.props.project} | |||
/>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,222 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { sortBy } from 'lodash'; | |||
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; | |||
import { Branch, Component } from '../../../types'; | |||
import { getBranches } from '../../../../api/branches'; | |||
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
interface Props { | |||
branch: Branch; | |||
onClose: () => void; | |||
project: Component; | |||
} | |||
interface State { | |||
branches: Branch[]; | |||
loading: boolean; | |||
query: string; | |||
selected: string | null; | |||
} | |||
export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> { | |||
private mounted: boolean; | |||
private node: HTMLElement | null; | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
branches: [], | |||
loading: true, | |||
query: '', | |||
selected: null | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchBranches(); | |||
window.addEventListener('click', this.handleClickOutside); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
window.removeEventListener('click', this.handleClickOutside); | |||
} | |||
fetchBranches = () => { | |||
this.setState({ loading: true }); | |||
getBranches(this.props.project.key).then( | |||
(branches: Branch[]) => { | |||
if (this.mounted) { | |||
this.setState({ branches: this.sortBranches(branches), loading: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
sortBranches = (branches: Branch[]): Branch[] => | |||
sortBy( | |||
branches, | |||
branch => !branch.isMain, // main branch first | |||
branch => !isShortLivingBranch(branch), // then short-living branches | |||
branch => getBranchDisplayName(branch) // then by name | |||
); | |||
getFilteredBranches = () => | |||
this.state.branches.filter(branch => | |||
getBranchDisplayName(branch).toLowerCase().includes(this.state.query.toLowerCase()) | |||
); | |||
handleClickOutside = (event: Event) => { | |||
if (!this.node || !this.node.contains(event.target as HTMLElement)) { | |||
this.props.onClose(); | |||
} | |||
}; | |||
handleSearchChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ query: event.currentTarget.value, selected: null }); | |||
handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { | |||
switch (event.keyCode) { | |||
case 13: | |||
event.preventDefault(); | |||
this.openSelected(); | |||
return; | |||
case 27: | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
return; | |||
case 38: | |||
event.preventDefault(); | |||
this.selectPrevious(); | |||
return; | |||
case 40: | |||
event.preventDefault(); | |||
this.selectNext(); | |||
return; | |||
} | |||
}; | |||
openSelected = () => { | |||
const selected = this.getSelected(); | |||
const branch = this.getFilteredBranches().find( | |||
branch => getBranchDisplayName(branch) === selected | |||
); | |||
if (branch) { | |||
this.context.router.push(this.getProjectBranchUrl(branch)); | |||
} | |||
}; | |||
selectPrevious = () => { | |||
const selected = this.getSelected(); | |||
const branches = this.getFilteredBranches(); | |||
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); | |||
if (index > 0) { | |||
this.setState({ selected: getBranchDisplayName(branches[index - 1]) }); | |||
} | |||
}; | |||
selectNext = () => { | |||
const selected = this.getSelected(); | |||
const branches = this.getFilteredBranches(); | |||
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); | |||
if (index >= 0 && index < branches.length - 1) { | |||
this.setState({ selected: getBranchDisplayName(branches[index + 1]) }); | |||
} | |||
}; | |||
handleSelect = (branch: Branch) => { | |||
this.setState({ selected: getBranchDisplayName(branch) }); | |||
}; | |||
getSelected = () => { | |||
const branches = this.getFilteredBranches(); | |||
return this.state.selected || (branches.length > 0 && getBranchDisplayName(branches[0])); | |||
}; | |||
getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch); | |||
isSelected = (branch: Branch) => getBranchDisplayName(branch) === this.getSelected(); | |||
renderSearch = () => | |||
<div className="search-box menu-search"> | |||
<button className="search-box-submit button-clean"> | |||
<i className="icon-search-new" /> | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={this.handleSearchChange} | |||
onKeyDown={this.handleKeyDown} | |||
placeholder={translate('search_verb')} | |||
type="search" | |||
value={this.state.query} | |||
/> | |||
</div>; | |||
renderBranchesList = () => { | |||
const branches = this.getFilteredBranches(); | |||
const selected = this.getSelected(); | |||
return branches.length > 0 | |||
? <ul className="menu"> | |||
{branches.map(branch => | |||
<ComponentNavBranchesMenuItem | |||
branch={branch} | |||
component={this.props.project} | |||
key={getBranchDisplayName(branch)} | |||
onSelect={this.handleSelect} | |||
selected={getBranchDisplayName(branch) === selected} | |||
/> | |||
)} | |||
</ul> | |||
: <div className="menu-message note"> | |||
{translate('no_results')} | |||
</div>; | |||
}; | |||
render() { | |||
return ( | |||
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}> | |||
{this.state.loading | |||
? <i className="spinner" /> | |||
: <div> | |||
{this.renderSearch()} | |||
{this.renderBranchesList()} | |||
</div>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as classNames from 'classnames'; | |||
import BranchStatus from './BranchStatus'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
onSelect: (branch: Branch) => void; | |||
selected: boolean; | |||
} | |||
export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { | |||
const displayName = getBranchDisplayName(branch); | |||
const handleMouseEnter = () => { | |||
props.onSelect(branch); | |||
}; | |||
return ( | |||
<li key={displayName} onMouseEnter={handleMouseEnter}> | |||
<Link | |||
className={classNames('navbar-context-meta-branch-menu-item', { | |||
active: props.selected | |||
})} | |||
to={getProjectBranchUrl(props.component.key, branch)}> | |||
<div> | |||
<BranchIcon | |||
className={classNames('little-spacer-right', { | |||
'big-spacer-left': isShortLivingBranch(branch) | |||
})} | |||
/> | |||
{displayName} | |||
</div> | |||
<div className="big-spacer-left note"> | |||
<BranchStatus branch={branch} concise={true} /> | |||
</div> | |||
</Link> | |||
</li> | |||
); | |||
} |
@@ -17,11 +17,12 @@ | |||
* 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 PropTypes from 'prop-types'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import classNames from 'classnames'; | |||
import * as classNames from 'classnames'; | |||
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
const SETTINGS_URLS = [ | |||
@@ -38,12 +39,13 @@ const SETTINGS_URLS = [ | |||
'/project/deletion' | |||
]; | |||
export default class ComponentNavMenu extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
conf: PropTypes.object.isRequired | |||
}; | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
} | |||
export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
isProject() { | |||
return this.props.component.qualifier === 'TRK'; | |||
} | |||
@@ -62,6 +64,10 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
} | |||
renderDashboardLink() { | |||
if (isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
} | |||
const pathname = this.isView() ? '/portfolio' : '/dashboard'; | |||
return ( | |||
<li> | |||
@@ -80,7 +86,10 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/code', query: { id: this.props.component.key } }} | |||
to={{ | |||
pathname: '/code', | |||
query: { branch: this.props.branch.name, id: this.props.component.key } | |||
}} | |||
activeClassName="active"> | |||
{this.isView() || this.isApplication() | |||
? translate('view_projects.page') | |||
@@ -95,6 +104,10 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
return null; | |||
} | |||
if (isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
} | |||
return ( | |||
<li> | |||
<Link | |||
@@ -112,7 +125,11 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
<Link | |||
to={{ | |||
pathname: '/project/issues', | |||
query: { id: this.props.component.key, resolved: 'false' } | |||
query: { | |||
branch: this.props.branch.name, | |||
id: this.props.component.key, | |||
resolved: 'false' | |||
} | |||
}} | |||
activeClassName="active"> | |||
{translate('issues.page')} | |||
@@ -122,6 +139,10 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
} | |||
renderComponentMeasuresLink() { | |||
if (isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
} | |||
return ( | |||
<li> | |||
<Link | |||
@@ -134,6 +155,10 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
} | |||
renderAdministration() { | |||
if (isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
} | |||
const adminLinks = this.renderAdministrationLinks(); | |||
if (!adminLinks.some(link => link != null)) { | |||
return null; | |||
@@ -314,7 +339,7 @@ export default class ComponentNavMenu extends React.PureComponent { | |||
); | |||
} | |||
renderExtension = ({ key, name }, isAdmin) => { | |||
renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => { | |||
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; | |||
return ( | |||
<li key={key}> |
@@ -17,18 +17,31 @@ | |||
* 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 DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; | |||
import * as React from 'react'; | |||
import IncrementalBadge from './IncrementalBadge'; | |||
import PendingIcon from '../../../../components/shared/pending-icon'; | |||
import BranchStatus from './BranchStatus'; | |||
import { Branch, Component, ComponentConfiguration } from '../../../types'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import PendingIcon from '../../../../components/icons-components/PendingIcon'; | |||
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
export default function ComponentNavMeta(props) { | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
incremental?: boolean; | |||
isInProgress?: boolean; | |||
isFailed?: boolean; | |||
isPending?: boolean; | |||
} | |||
export default function ComponentNavMeta(props: Props) { | |||
const metaList = []; | |||
const canSeeBackgroundTasks = props.conf.showBackgroundTasks; | |||
const backgroundTasksUrl = | |||
window.baseUrl + `/project/background_tasks?id=${encodeURIComponent(props.component.key)}`; | |||
(window as any).baseUrl + | |||
`/project/background_tasks?id=${encodeURIComponent(props.component.key)}`; | |||
if (props.isInProgress) { | |||
const tooltip = canSeeBackgroundTasks | |||
@@ -76,18 +89,19 @@ export default function ComponentNavMeta(props) { | |||
</Tooltip> | |||
); | |||
} | |||
if (props.analysisDate) { | |||
if (props.component.analysisDate && props.branch.isMain) { | |||
metaList.push( | |||
<li key="analysisDate"> | |||
<DateTimeFormatter date={props.analysisDate} /> | |||
<DateTimeFormatter date={props.component.analysisDate} /> | |||
</li> | |||
); | |||
} | |||
if (props.version) { | |||
if (props.component.version && props.branch.isMain) { | |||
metaList.push( | |||
<li key="version"> | |||
Version {props.version} | |||
Version {props.component.version} | |||
</li> | |||
); | |||
} | |||
@@ -100,6 +114,14 @@ export default function ComponentNavMeta(props) { | |||
); | |||
} | |||
if (!props.branch.isMain) { | |||
metaList.push( | |||
<li className="navbar-context-meta-branch" key="branch-status"> | |||
<BranchStatus branch={props.branch} /> | |||
</li> | |||
); | |||
} | |||
return ( | |||
<div className="navbar-context-meta"> | |||
<ul className="list-inline"> |
@@ -17,7 +17,7 @@ | |||
* 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 * as React from 'react'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import BranchStatus from '../BranchStatus'; | |||
import { BranchType } from '../../../../types'; | |||
it('renders', () => { | |||
check(0, 0, 0); | |||
check(0, 1, 0); | |||
check(7, 3, 6); | |||
}); | |||
function check(bugs: number, codeSmells: number, vulnerabilities: number) { | |||
expect( | |||
shallow( | |||
<BranchStatus | |||
branch={{ | |||
isMain: false, | |||
name: 'foo', | |||
status: { bugs, codeSmells, vulnerabilities }, | |||
type: BranchType.SHORT | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
} |
@@ -0,0 +1,50 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavBranch from '../ComponentNavBranch'; | |||
import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../../types'; | |||
import { click } from '../../../../../helpers/testUtils'; | |||
it('renders main branch', () => { | |||
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
const component = {} as Component; | |||
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot(); | |||
}); | |||
it('renders short-living branch', () => { | |||
const branch: ShortLivingBranch = { | |||
isMain: false, | |||
name: 'foo', | |||
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, | |||
type: BranchType.SHORT | |||
}; | |||
const component = {} as Component; | |||
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot(); | |||
}); | |||
it('opens menu', () => { | |||
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
const component = {} as Component; | |||
const wrapper = shallow(<ComponentNavBranch branch={branch} project={component} />); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); | |||
}); |
@@ -0,0 +1,92 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavBranchesMenu from '../ComponentNavBranchesMenu'; | |||
import { | |||
BranchType, | |||
MainBranch, | |||
ShortLivingBranch, | |||
LongLivingBranch, | |||
Component | |||
} from '../../../../types'; | |||
import { elementKeydown } from '../../../../../helpers/testUtils'; | |||
it('renders list', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), longBranch('bar')], | |||
loading: false | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('searches', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], | |||
loading: false, | |||
query: 'bar' | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('selects next & previous', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], | |||
loading: false | |||
}); | |||
elementKeydown(wrapper.find('input'), 40); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foo'); | |||
elementKeydown(wrapper.find('input'), 40); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foobar'); | |||
elementKeydown(wrapper.find('input'), 38); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foo'); | |||
}); | |||
function mainBranch(): MainBranch { | |||
return { isMain: true, name: undefined, type: BranchType.LONG }; | |||
} | |||
function shortBranch(name: string): ShortLivingBranch { | |||
return { | |||
isMain: false, | |||
name, | |||
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, | |||
type: BranchType.SHORT | |||
}; | |||
} | |||
function longBranch(name: string): LongLivingBranch { | |||
return { isMain: false, name, type: BranchType.LONG }; | |||
} |
@@ -0,0 +1,58 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavBranchesMenuItem from '../ComponentNavBranchesMenuItem'; | |||
import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types'; | |||
it('renders main branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const mainBranch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={mainBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders short-living branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={shortBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -17,9 +17,10 @@ | |||
* 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 * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavMenu from '../ComponentNavMenu'; | |||
import { Branch, Component } from '../../../../types'; | |||
it('should work with extensions', () => { | |||
const component = { | |||
@@ -31,7 +32,11 @@ it('should work with extensions', () => { | |||
showSettings: true, | |||
extensions: [{ key: 'foo', name: 'Foo' }] | |||
}; | |||
expect(shallow(<ComponentNavMenu component={component} conf={conf} />)).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} /> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should work with multiple extensions', () => { | |||
@@ -47,5 +52,9 @@ it('should work with multiple extensions', () => { | |||
showSettings: true, | |||
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] | |||
}; | |||
expect(shallow(<ComponentNavMenu component={component} conf={conf} />)).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} /> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavMeta from '../ComponentNavMeta'; | |||
import { Branch, Component } from '../../../../types'; | |||
it('renders incremental badge', () => { | |||
check(true); | |||
@@ -28,7 +29,12 @@ it('renders incremental badge', () => { | |||
function check(incremental: boolean) { | |||
expect( | |||
shallow( | |||
<ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} /> | |||
<ComponentNavMeta | |||
branch={{} as Branch} | |||
component={{ key: 'foo' } as Component} | |||
conf={{}} | |||
incremental={incremental} | |||
/> | |||
).find('IncrementalBadge') | |||
).toHaveLength(incremental ? 1 : 0); | |||
} |
@@ -0,0 +1,91 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<i | |||
className="branch-status-indicator is-passed" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
</ul> | |||
`; | |||
exports[`renders 2`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<i | |||
className="branch-status-indicator is-failed" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
1 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
</ul> | |||
`; | |||
exports[`renders 3`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<i | |||
className="branch-status-indicator is-failed" | |||
/> | |||
</li> | |||
<li> | |||
7 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
6 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
<li> | |||
3 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
</li> | |||
</ul> | |||
`; |
@@ -0,0 +1,41 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders main branch 1`] = ` | |||
<div | |||
className="navbar-context-branches dropdown" | |||
> | |||
<a | |||
className="link-base-color link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<BranchIcon | |||
className="little-spacer-right" | |||
/> | |||
master | |||
<i | |||
className="icon-dropdown little-spacer-left" | |||
/> | |||
</a> | |||
</div> | |||
`; | |||
exports[`renders short-living branch 1`] = ` | |||
<div | |||
className="navbar-context-branches dropdown" | |||
> | |||
<a | |||
className="link-base-color link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<BranchIcon | |||
className="little-spacer-right" | |||
/> | |||
foo | |||
<i | |||
className="icon-dropdown little-spacer-left" | |||
/> | |||
</a> | |||
</div> | |||
`; |
@@ -0,0 +1,157 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders list 1`] = ` | |||
<div | |||
className="dropdown-menu dropdown-menu-shadow" | |||
> | |||
<div> | |||
<div | |||
className="search-box menu-search" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": undefined, | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
</div> | |||
`; | |||
exports[`searches 1`] = ` | |||
<div | |||
className="dropdown-menu dropdown-menu-shadow" | |||
> | |||
<div> | |||
<div | |||
className="search-box menu-search" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="bar" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "foobar", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,90 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders main branch 1`] = ` | |||
<li | |||
onMouseEnter={[Function]} | |||
> | |||
<Link | |||
className="navbar-context-meta-branch-menu-item" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "component", | |||
}, | |||
} | |||
} | |||
> | |||
<div> | |||
<BranchIcon | |||
className="little-spacer-right" | |||
/> | |||
master | |||
</div> | |||
<div | |||
className="big-spacer-left note" | |||
> | |||
<BranchStatus | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": undefined, | |||
"type": "LONG", | |||
} | |||
} | |||
concise={true} | |||
/> | |||
</div> | |||
</Link> | |||
</li> | |||
`; | |||
exports[`renders short-living branch 1`] = ` | |||
<li | |||
onMouseEnter={[Function]} | |||
> | |||
<Link | |||
className="navbar-context-meta-branch-menu-item" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": "foo", | |||
"id": "component", | |||
"resolved": "false", | |||
}, | |||
} | |||
} | |||
> | |||
<div> | |||
<BranchIcon | |||
className="little-spacer-right big-spacer-left" | |||
/> | |||
foo | |||
</div> | |||
<div | |||
className="big-spacer-left note" | |||
> | |||
<BranchStatus | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 1, | |||
"codeSmells": 2, | |||
"vulnerabilities": 3, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
concise={true} | |||
/> | |||
</div> | |||
</Link> | |||
</li> | |||
`; |
@@ -28,6 +28,7 @@ exports[`should work with extensions 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
"resolved": "false", | |||
}, | |||
@@ -63,6 +64,7 @@ exports[`should work with extensions 1`] = ` | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -227,6 +229,7 @@ exports[`should work with multiple extensions 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
"resolved": "false", | |||
}, | |||
@@ -262,6 +265,7 @@ exports[`should work with multiple extensions 1`] = ` | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -93,5 +93,4 @@ | |||
padding: 0; | |||
overflow-y: auto; | |||
overflow-x: hidden; | |||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175); | |||
} |
@@ -354,7 +354,7 @@ export default class Search extends React.PureComponent { | |||
{this.state.open && | |||
Object.keys(this.state.results).length > 0 && | |||
<div | |||
className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown" | |||
className="dropdown-menu dropdown-menu-shadow dropdown-menu-right global-navbar-search-dropdown" | |||
ref={node => (this.node = node)}> | |||
<SearchResults | |||
allowMore={this.state.query.length !== 1} |
@@ -38,7 +38,6 @@ import { | |||
} from '../../../api/ce'; | |||
import { updateTask, mapFiltersToParameters } from '../utils'; | |||
/*:: import type { Task } from '../types'; */ | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import '../background-tasks.css'; | |||
import { fetchOrganizations } from '../../../store/rootActions'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -257,12 +256,6 @@ class BackgroundTasksApp extends React.PureComponent { | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: ownProps.location.query.id | |||
? getComponent(state, ownProps.location.query.id) | |||
: undefined | |||
}); | |||
const mapDispatchToProps = { fetchOrganizations }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(BackgroundTasksApp); | |||
export default connect(null, mapDispatchToProps)(BackgroundTasksApp); |
@@ -20,7 +20,7 @@ | |||
/* @flow */ | |||
import React from 'react'; | |||
import { STATUSES } from './../constants'; | |||
import PendingIcon from '../../../components/shared/pending-icon'; | |||
import PendingIcon from '../../../components/icons-components/PendingIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Task } from '../types'; */ | |||
@@ -20,7 +20,6 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import Components from './Components'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import SourceViewer from './../../../components/SourceViewer/SourceViewer'; | |||
@@ -33,11 +32,10 @@ import { | |||
parseError | |||
} from '../utils'; | |||
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../code.css'; | |||
class App extends React.PureComponent { | |||
export default class App extends React.PureComponent { | |||
state = { | |||
loading: true, | |||
baseComponent: null, | |||
@@ -75,7 +73,7 @@ class App extends React.PureComponent { | |||
this.setState({ loading: true }); | |||
const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); | |||
retrieveComponentChildren(component.key, isPortfolio) | |||
retrieveComponentChildren(component.key, isPortfolio, component.branch) | |||
.then(r => { | |||
addComponent(r.baseComponent); | |||
this.handleUpdate(); | |||
@@ -92,7 +90,7 @@ class App extends React.PureComponent { | |||
this.setState({ loading: true }); | |||
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); | |||
retrieveComponent(componentKey, isPortfolio) | |||
retrieveComponent(componentKey, isPortfolio, this.props.component.branch) | |||
.then(r => { | |||
if (this.mounted) { | |||
if (['FIL', 'UTS'].includes(r.component.qualifier)) { | |||
@@ -132,10 +130,10 @@ class App extends React.PureComponent { | |||
this.loadComponent(finalKey); | |||
} | |||
handleLoadMore() { | |||
handleLoadMore = () => { | |||
const { baseComponent, page } = this.state; | |||
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); | |||
loadMoreChildren(baseComponent.key, page + 1, isPortfolio) | |||
loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.component.branch) | |||
.then(r => { | |||
if (this.mounted) { | |||
this.setState({ | |||
@@ -148,16 +146,16 @@ class App extends React.PureComponent { | |||
.catch(e => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
parseError(e).then(this.handleError.bind(this)); | |||
parseError(e).then(this.handleError); | |||
} | |||
}); | |||
} | |||
}; | |||
handleError(error) { | |||
handleError = error => { | |||
if (this.mounted) { | |||
this.setState({ error }); | |||
} | |||
} | |||
}; | |||
render() { | |||
const { component, location } = this.props; | |||
@@ -186,7 +184,7 @@ class App extends React.PureComponent { | |||
{error} | |||
</div>} | |||
<Search location={location} component={component} onError={this.handleError.bind(this)} /> | |||
<Search location={location} component={component} onError={this.handleError} /> | |||
<div className="code-components"> | |||
{shouldShowBreadcrumbs && | |||
@@ -202,24 +200,14 @@ class App extends React.PureComponent { | |||
</div>} | |||
{shouldShowComponents && | |||
<ListFooter | |||
count={components.length} | |||
total={total} | |||
loadMore={this.handleLoadMore.bind(this)} | |||
/>} | |||
<ListFooter count={components.length} total={total} loadMore={this.handleLoadMore} />} | |||
{shouldShowSourceViewer && | |||
<div className="spacer-top"> | |||
<SourceViewer component={sourceViewer.key} /> | |||
<SourceViewer branch={component.branch} component={sourceViewer.key} /> | |||
</div>} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(App); |
@@ -70,10 +70,10 @@ export default class Component extends React.PureComponent { | |||
switch (component.qualifier) { | |||
case 'FIL': | |||
case 'UTS': | |||
componentAction = <ComponentPin component={component} />; | |||
componentAction = <ComponentPin branch={rootComponent.branch} component={component} />; | |||
break; | |||
default: | |||
componentAction = <ComponentDetach component={component} />; | |||
componentAction = <ComponentDetach branch={rootComponent.branch} component={component} />; | |||
} | |||
} | |||
@@ -21,10 +21,10 @@ import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function ComponentDetach({ component }) { | |||
export default function ComponentDetach({ component, branch }) { | |||
return ( | |||
<Link | |||
to={{ pathname: '/dashboard', query: { id: component.refKey || component.key } }} | |||
to={{ pathname: '/dashboard', query: { branch, id: component.refKey || component.key } }} | |||
className="icon-detach" | |||
title={translate('code.open_component_page')} | |||
/> |
@@ -71,7 +71,7 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { | |||
</Link> | |||
); | |||
} else if (canBrowse) { | |||
const query = { id: rootComponent.key }; | |||
const query = { id: rootComponent.key, branch: rootComponent.branch }; | |||
if (component.key !== rootComponent.key) { | |||
Object.assign(query, { selected: component.key }); | |||
} |
@@ -22,10 +22,10 @@ import Workspace from '../../../components/workspace/main'; | |||
import PinIcon from '../../../components/shared/pin-icon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const ComponentPin = ({ component }) => { | |||
const ComponentPin = ({ branch, component }) => { | |||
const handleClick = e => { | |||
e.preventDefault(); | |||
Workspace.openComponent({ key: component.key }); | |||
Workspace.openComponent({ branch, key: component.key }); | |||
}; | |||
return ( |
@@ -46,7 +46,7 @@ export default class Search extends React.PureComponent { | |||
}; | |||
componentWillMount() { | |||
this.handleSearch = debounce(this.handleSearch.bind(this), 250); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
componentDidMount() { | |||
@@ -100,6 +100,7 @@ export default class Search extends React.PureComponent { | |||
this.context.router.push({ | |||
pathname: '/code', | |||
query: { | |||
branch: component.branch, | |||
id: component.key, | |||
selected: selected.key | |||
} | |||
@@ -126,7 +127,7 @@ export default class Search extends React.PureComponent { | |||
} | |||
} | |||
handleSearch(query) { | |||
handleSearch = query => { | |||
// first time check if value has changed due to debounce | |||
if (this.mounted && this.checkInputValue(query)) { | |||
const { component, onError } = this.props; | |||
@@ -135,7 +136,12 @@ export default class Search extends React.PureComponent { | |||
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); | |||
const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; | |||
getTree(component.key, { q: query, s: 'qualifier,name', qualifiers }) | |||
getTree(component.key, { | |||
branch: component.branch, | |||
q: query, | |||
s: 'qualifier,name', | |||
qualifiers | |||
}) | |||
.then(r => { | |||
// second time check if value has change due to api request | |||
if (this.mounted && this.checkInputValue(query)) { | |||
@@ -154,7 +160,7 @@ export default class Search extends React.PureComponent { | |||
} | |||
}); | |||
} | |||
} | |||
}; | |||
handleQueryChange(query) { | |||
this.setState({ query }); |
@@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps } from 'react-router'; | |||
const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/App').then(i => callback(null, { component: i.default })); | |||
import('./components/App').then(i => callback(null, { component: (i as any).default })); | |||
} | |||
} | |||
]; |
@@ -120,7 +120,7 @@ function getMetrics(isPortfolio) { | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
function retrieveComponentBase(componentKey, isPortfolio) { | |||
function retrieveComponentBase(componentKey, isPortfolio, branch) { | |||
const existing = getComponentFromBucket(componentKey); | |||
if (existing) { | |||
return Promise.resolve(existing); | |||
@@ -128,7 +128,7 @@ function retrieveComponentBase(componentKey, isPortfolio) { | |||
const metrics = getMetrics(isPortfolio); | |||
return getComponent(componentKey, metrics).then(component => { | |||
return getComponent(componentKey, metrics, branch).then(component => { | |||
addComponent(component); | |||
return component; | |||
}); | |||
@@ -139,7 +139,7 @@ function retrieveComponentBase(componentKey, isPortfolio) { | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
export function retrieveComponentChildren(componentKey, isPortfolio) { | |||
export function retrieveComponentChildren(componentKey, isPortfolio, branch) { | |||
const existing = getComponentChildren(componentKey); | |||
if (existing) { | |||
return Promise.resolve({ | |||
@@ -151,7 +151,7 @@ export function retrieveComponentChildren(componentKey, isPortfolio) { | |||
const metrics = getMetrics(isPortfolio); | |||
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, s: 'qualifier,name' }) | |||
return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' }) | |||
.then(prepareChildren) | |||
.then(expandRootDir(metrics)) | |||
.then(r => { | |||
@@ -162,13 +162,13 @@ export function retrieveComponentChildren(componentKey, isPortfolio) { | |||
}); | |||
} | |||
function retrieveComponentBreadcrumbs(componentKey) { | |||
function retrieveComponentBreadcrumbs(componentKey, branch) { | |||
const existing = getComponentBreadcrumbs(componentKey); | |||
if (existing) { | |||
return Promise.resolve(existing); | |||
} | |||
return getBreadcrumbs(componentKey).then(skipRootDir).then(breadcrumbs => { | |||
return getBreadcrumbs(componentKey, branch).then(skipRootDir).then(breadcrumbs => { | |||
addComponentBreadcrumbs(componentKey, breadcrumbs); | |||
return breadcrumbs; | |||
}); | |||
@@ -179,11 +179,11 @@ function retrieveComponentBreadcrumbs(componentKey) { | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
export function retrieveComponent(componentKey, isPortfolio) { | |||
export function retrieveComponent(componentKey, isPortfolio, branch) { | |||
return Promise.all([ | |||
retrieveComponentBase(componentKey, isPortfolio), | |||
retrieveComponentChildren(componentKey, isPortfolio), | |||
retrieveComponentBreadcrumbs(componentKey) | |||
retrieveComponentBase(componentKey, isPortfolio, branch), | |||
retrieveComponentChildren(componentKey, isPortfolio, branch), | |||
retrieveComponentBreadcrumbs(componentKey, branch) | |||
]).then(r => { | |||
return { | |||
component: r[0], | |||
@@ -195,10 +195,10 @@ export function retrieveComponent(componentKey, isPortfolio) { | |||
}); | |||
} | |||
export function loadMoreChildren(componentKey, page, isPortfolio) { | |||
export function loadMoreChildren(componentKey, page, isPortfolio, branch) { | |||
const metrics = getMetrics(isPortfolio); | |||
return getChildren(componentKey, metrics, { ps: PAGE_SIZE, p: page }) | |||
return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page }) | |||
.then(prepareChildren) | |||
.then(expandRootDir(metrics)) | |||
.then(r => { |
@@ -22,12 +22,7 @@ import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import App from './App'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { | |||
getComponent, | |||
getCurrentUser, | |||
getMetrics, | |||
getMetricsKey | |||
} from '../../../store/rootReducer'; | |||
import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer'; | |||
import { fetchMetrics } from '../../../store/rootActions'; | |||
import { getMeasuresAndMeta } from '../../../api/measures'; | |||
import { getLeakPeriod } from '../../../helpers/periods'; | |||
@@ -35,8 +30,7 @@ import { enhanceMeasure } from '../../../components/measure/utils'; | |||
/*:: import type { Component, Period } from '../types'; */ | |||
/*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */ | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
const mapStateToProps = state => ({ | |||
currentUser: getCurrentUser(state), | |||
metrics: getMetrics(state), | |||
metricsKey: getMetricsKey(state) |
@@ -19,12 +19,10 @@ | |||
*/ | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import init from '../init'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class CustomMeasuresAppContainer extends React.PureComponent { | |||
export default class CustomMeasuresAppContainer extends React.PureComponent { | |||
componentDidMount() { | |||
init(this.refs.container, this.props.component); | |||
} | |||
@@ -38,9 +36,3 @@ class CustomMeasuresAppContainer extends React.PureComponent { | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(CustomMeasuresAppContainer); |
@@ -23,7 +23,7 @@ const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/CustomMeasuresAppContainer').then(i => | |||
callback(null, { component: i.default }) | |||
callback(null, { component: (i as any).default }) | |||
); | |||
} | |||
} |
@@ -63,6 +63,7 @@ import '../styles.css'; | |||
/*:: | |||
export type Props = { | |||
branch?: { name: string }, | |||
component?: Component, | |||
currentUser: CurrentUser, | |||
fetchIssues: (query: RawQuery) => Promise<*>, | |||
@@ -171,6 +172,7 @@ export default class App extends React.PureComponent { | |||
const { query } = this.props.location; | |||
const { query: prevQuery } = prevProps.location; | |||
if ( | |||
prevProps.component !== this.props.component || | |||
!areQueriesEqual(prevQuery, query) || | |||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) | |||
) { | |||
@@ -306,6 +308,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
branch: this.props.branch && this.props.branch.name, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: issue | |||
@@ -324,6 +327,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
branch: this.props.branch && this.props.branch.name, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: undefined | |||
@@ -359,6 +363,8 @@ export default class App extends React.PureComponent { | |||
: undefined; | |||
const parameters = { | |||
branch: this.props.branch && this.props.branch.name, | |||
componentKeys: component && component.key, | |||
s: 'FILE_LINE', | |||
...serializeQuery(query), | |||
ps: '100', | |||
@@ -367,10 +373,6 @@ export default class App extends React.PureComponent { | |||
...additional | |||
}; | |||
if (component) { | |||
Object.assign(parameters, { componentKeys: component.key }); | |||
} | |||
// only sorting by CREATION_DATE is allowed, so let's sort DESC | |||
if (query.sort) { | |||
Object.assign(parameters, { asc: 'false' }); | |||
@@ -552,6 +554,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, ...changes }), | |||
branch: this.props.branch && this.props.branch.name, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
@@ -567,6 +570,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), | |||
branch: this.props.branch && this.props.branch.name, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: myIssues ? 'true' : undefined | |||
} | |||
@@ -593,6 +597,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...DEFAULT_QUERY, | |||
branch: this.props.branch && this.props.branch.name, | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
@@ -885,6 +890,8 @@ export default class App extends React.PureComponent { | |||
<div> | |||
{openIssue | |||
? <IssuesSourceViewer | |||
branch={this.props.branch} | |||
component={component} | |||
openIssue={openIssue} | |||
loadIssues={this.fetchIssuesForComponent} | |||
onIssueChange={this.handleIssueChange} |
@@ -24,17 +24,14 @@ import { withRouter } from 'react-router'; | |||
import { uniq } from 'lodash'; | |||
import App from './App'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { getComponent, getCurrentUser } from '../../../store/rootReducer'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { getOrganizations } from '../../../api/organizations'; | |||
import { receiveOrganizations } from '../../../store/organizations/duck'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: ownProps.location.query.id | |||
? getComponent(state, ownProps.location.query.id) | |||
: undefined, | |||
const mapStateToProps = state => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
@@ -21,10 +21,13 @@ | |||
import React from 'react'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
/*:: import type { Component, } from '../utils'; */ | |||
/*:: import type { Issue } from '../../../components/issue/types'; */ | |||
/*:: | |||
type Props = {| | |||
branch?: { name: string }, | |||
component: Component, | |||
loadIssues: (string, number, number) => Promise<*>, | |||
onIssueChange: Issue => void, | |||
onIssueSelect: string => void, | |||
@@ -83,6 +86,7 @@ export default class IssuesSourceViewer extends React.PureComponent { | |||
<div ref={node => (this.node = node)}> | |||
<SourceViewer | |||
aroundLine={openIssue.textRange ? openIssue.textRange.endLine : undefined} | |||
branch={this.props.branch && this.props.branch.name} | |||
component={openIssue.component} | |||
displayAllIssues={true} | |||
highlightedLocations={locations} |
@@ -215,6 +215,10 @@ export default class CreationDateFacet extends React.PureComponent { | |||
renderPredefinedPeriods() { | |||
const { component, createdInLast, sinceLeakPeriod } = this.props; | |||
if (component != null && component.branch != null) { | |||
// FIXME handle long-living branches | |||
return null; | |||
} | |||
return ( | |||
<div className="spacer-top issues-predefined-periods"> | |||
<FacetItem |
@@ -22,10 +22,13 @@ import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import OverviewApp from './OverviewApp'; | |||
import EmptyOverview from './EmptyOverview'; | |||
import { isShortLivingBranch } from '../../../helpers/branches'; | |||
import { getProjectBranchUrl } from '../../../helpers/urls'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
/*:: | |||
type Props = { | |||
branch: {}, | |||
component: { | |||
analysisDate?: string, | |||
id: string, | |||
@@ -33,6 +36,7 @@ type Props = { | |||
qualifier: string, | |||
tags: Array<string> | |||
}, | |||
onComponentChange: {} => void, | |||
router: Object | |||
}; | |||
*/ | |||
@@ -52,6 +56,9 @@ export default class App extends React.PureComponent { | |||
query: { id: this.props.component.key } | |||
}); | |||
} | |||
if (isShortLivingBranch(this.props.branch)) { | |||
this.context.router.replace(getProjectBranchUrl(this.props.component.key, this.props.branch)); | |||
} | |||
} | |||
isPortfolio() { | |||
@@ -59,7 +66,7 @@ export default class App extends React.PureComponent { | |||
} | |||
render() { | |||
if (this.isPortfolio()) { | |||
if (this.isPortfolio() || isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
} | |||
@@ -77,6 +84,6 @@ export default class App extends React.PureComponent { | |||
return <EmptyOverview component={component} />; | |||
} | |||
return <OverviewApp component={component} />; | |||
return <OverviewApp component={component} onComponentChange={this.props.onComponentChange} />; | |||
} | |||
} |
@@ -41,7 +41,8 @@ import '../styles.css'; | |||
/*:: | |||
type Props = { | |||
component: Component | |||
component: Component, | |||
onComponentChange: {} => void | |||
}; | |||
*/ | |||
@@ -175,7 +176,12 @@ export default class OverviewApp extends React.PureComponent { | |||
</div> | |||
<div className="page-sidebar-fixed"> | |||
<Meta component={component} history={history} measures={measures} /> | |||
<Meta | |||
component={component} | |||
history={history} | |||
measures={measures} | |||
onComponentChange={this.props.onComponentChange} | |||
/> | |||
</div> | |||
</div> | |||
</div> |
@@ -30,7 +30,14 @@ import MetaSize from './MetaSize'; | |||
import MetaTags from './MetaTags'; | |||
import { areThereCustomOrganizations } from '../../../store/rootReducer'; | |||
const Meta = ({ component, history, measures, areThereCustomOrganizations, router }) => { | |||
const Meta = ({ | |||
component, | |||
history, | |||
measures, | |||
areThereCustomOrganizations, | |||
onComponentChange, | |||
router | |||
}) => { | |||
const { qualifier, description, qualityProfiles, qualityGate } = component; | |||
const isProject = qualifier === 'TRK'; | |||
@@ -53,7 +60,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route | |||
<MetaSize component={component} measures={measures} /> | |||
{isProject && <MetaTags component={component} />} | |||
{isProject && <MetaTags component={component} onComponentChange={onComponentChange} />} | |||
{(isProject || isApplication) && | |||
<AnalysesList |
@@ -19,9 +19,10 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { setProjectTags } from '../../../api/components'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import TagsList from '../../../components/tags/TagsList'; | |||
import ProjectTagsSelectorContainer from '../../projects/components/ProjectTagsSelectorContainer'; | |||
import MetaTagsSelector from './MetaTagsSelector'; | |||
/*:: | |||
type Props = { | |||
@@ -31,7 +32,8 @@ type Props = { | |||
configuration?: { | |||
showSettings?: boolean | |||
} | |||
} | |||
}, | |||
onComponentChange: {} => void | |||
}; | |||
*/ | |||
@@ -104,6 +106,13 @@ export default class MetaTags extends React.PureComponent { | |||
}; | |||
} | |||
handleSetProjectTags = (tags /*: Array<string> */) => { | |||
setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then( | |||
() => this.props.onComponentChange({ tags }), | |||
() => {} | |||
); | |||
}; | |||
render() { | |||
const { tags, key } = this.props.component; | |||
const { popupOpen, popupPosition } = this.state; | |||
@@ -119,10 +128,11 @@ export default class MetaTags extends React.PureComponent { | |||
</button> | |||
{popupOpen && | |||
<div ref={tagsSelector => (this.tagsSelector = tagsSelector)}> | |||
<ProjectTagsSelectorContainer | |||
<MetaTagsSelector | |||
position={popupPosition} | |||
project={key} | |||
selectedTags={tags} | |||
setProjectTags={this.handleSetProjectTags} | |||
/> | |||
</div>} | |||
</div> |
@@ -19,18 +19,16 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { debounce, without } from 'lodash'; | |||
import TagsSelector from '../../../components/tags/TagsSelector'; | |||
import { searchProjectTags } from '../../../api/components'; | |||
import { setProjectTags } from '../store/actions'; | |||
/*:: | |||
type Props = { | |||
position: {}, | |||
project: string, | |||
selectedTags: Array<string>, | |||
setProjectTags: (string, Array<string>) => void | |||
setProjectTags: (Array<string>) => void | |||
}; | |||
*/ | |||
@@ -42,7 +40,7 @@ type State = { | |||
const LIST_SIZE = 10; | |||
class ProjectTagsSelectorContainer extends React.PureComponent { | |||
export default class MetaTagsSelector extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
@@ -68,11 +66,11 @@ class ProjectTagsSelectorContainer extends React.PureComponent { | |||
}; | |||
onSelect = (tag /*: string */) => { | |||
this.props.setProjectTags(this.props.project, [...this.props.selectedTags, tag]); | |||
this.props.setProjectTags([...this.props.selectedTags, tag]); | |||
}; | |||
onUnselect = (tag /*: string */) => { | |||
this.props.setProjectTags(this.props.project, without(this.props.selectedTags, tag)); | |||
this.props.setProjectTags(without(this.props.selectedTags, tag)); | |||
}; | |||
render() { | |||
@@ -89,5 +87,3 @@ class ProjectTagsSelectorContainer extends React.PureComponent { | |||
); | |||
} | |||
} | |||
export default connect(null, { setProjectTags })(ProjectTagsSelectorContainer); |
@@ -0,0 +1,61 @@ | |||
/* | |||
* 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. | |||
*/ | |||
/* eslint-disable import/order, import/first */ | |||
import * as React from 'react'; | |||
import { mount, shallow } from 'enzyme'; | |||
import MetaTagsSelector from '../MetaTagsSelector'; | |||
jest.mock('../../../../api/components', () => ({ | |||
searchProjectTags: jest.fn() | |||
})); | |||
jest.useFakeTimers(); | |||
import { searchProjectTags } from '../../../../api/components'; | |||
it('searches tags on mount', () => { | |||
searchProjectTags.mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] })); | |||
mount( | |||
<MetaTagsSelector position={{}} project="foo" selectedTags={[]} setProjectTags={jest.fn()} /> | |||
); | |||
jest.runAllTimers(); | |||
expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); | |||
}); | |||
it('selects and deselects tags', () => { | |||
const setProjectTags = jest.fn(); | |||
const wrapper = shallow( | |||
<MetaTagsSelector | |||
position={{}} | |||
project="foo" | |||
selectedTags={['foo', 'bar']} | |||
setProjectTags={setProjectTags} | |||
/> | |||
); | |||
wrapper.find('TagsSelector').prop('onSelect')('baz'); | |||
expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']); | |||
// note that the `selectedTags` is a prop and so it wasn't changed | |||
wrapper.find('TagsSelector').prop('onUnselect')('bar'); | |||
expect(setProjectTags).toHaveBeenLastCalledWith(['foo']); | |||
}); |
@@ -40,7 +40,7 @@ exports[`should open the tag selector on click 2`] = ` | |||
/> | |||
</button> | |||
<div> | |||
<Connect(ProjectTagsSelectorContainer) | |||
<MetaTagsSelector | |||
position={ | |||
Object { | |||
"right": 0, | |||
@@ -54,6 +54,7 @@ exports[`should open the tag selector on click 2`] = ` | |||
"bar", | |||
] | |||
} | |||
setProjectTags={[Function]} | |||
/> | |||
</div> | |||
</div> |
@@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps } from 'react-router'; | |||
const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/AppContainer').then(i => callback(null, { component: i.default })); | |||
import('./components/App').then(i => callback(null, { component: (i as any).default })); | |||
} | |||
} | |||
]; |
@@ -18,36 +18,17 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import Header from './Header'; | |||
import Form from './Form'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class Deletion extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object | |||
}; | |||
render() { | |||
if (!this.props.component) { | |||
return null; | |||
} | |||
return ( | |||
<div className="page page-limited"> | |||
<Helmet title={translate('deletion.page')} /> | |||
<Header component={this.props.component} /> | |||
<Form component={this.props.component} /> | |||
</div> | |||
); | |||
} | |||
export default function Deletion(props) { | |||
return ( | |||
<div className="page page-limited"> | |||
<Helmet title={translate('deletion.page')} /> | |||
<Header component={props.component} /> | |||
<Form component={props.component} /> | |||
</div> | |||
); | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(Deletion); |
@@ -35,11 +35,11 @@ import { | |||
import { parseError } from '../../code/utils'; | |||
import { reloadUpdateKeyPage } from './utils'; | |||
import RecentHistory from '../../../app/components/RecentHistory'; | |||
import { getProjectAdminProjectModules, getComponent } from '../../../store/rootReducer'; | |||
import { getProjectAdminProjectModules } from '../../../store/rootReducer'; | |||
class Key extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
component: PropTypes.object, | |||
fetchProjectModules: PropTypes.func.isRequired, | |||
changeKey: PropTypes.func.isRequired, | |||
addGlobalErrorMessage: PropTypes.func.isRequired, | |||
@@ -141,7 +141,6 @@ class Key extends React.PureComponent { | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
modules: getProjectAdminProjectModules(state, ownProps.location.query.id) | |||
}); | |||
@@ -25,12 +25,12 @@ import Header from './Header'; | |||
import Table from './Table'; | |||
import DeletionModal from './views/DeletionModal'; | |||
import { fetchProjectLinks, deleteProjectLink, createProjectLink } from '../store/actions'; | |||
import { getProjectAdminProjectLinks, getComponent } from '../../../store/rootReducer'; | |||
import { getProjectAdminProjectLinks } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class Links extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
component: PropTypes.object, | |||
links: PropTypes.array | |||
}; | |||
@@ -67,7 +67,6 @@ class Links extends React.PureComponent { | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
links: getProjectAdminProjectLinks(state, ownProps.location.query.id) | |||
}); | |||
@@ -24,16 +24,12 @@ import { connect } from 'react-redux'; | |||
import Header from './Header'; | |||
import Form from './Form'; | |||
import { fetchProjectGate, setProjectGate } from '../store/actions'; | |||
import { | |||
getProjectAdminAllGates, | |||
getProjectAdminProjectGate, | |||
getComponent | |||
} from '../../../store/rootReducer'; | |||
import { getProjectAdminAllGates, getProjectAdminProjectGate } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class QualityGate extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
component: PropTypes.object, | |||
allGates: PropTypes.array, | |||
gate: PropTypes.object | |||
}; | |||
@@ -62,7 +58,6 @@ class QualityGate extends React.PureComponent { | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
allGates: getProjectAdminAllGates(state), | |||
gate: getProjectAdminProjectGate(state, ownProps.location.query.id) | |||
}); |
@@ -27,8 +27,7 @@ import { fetchProjectProfiles, setProjectProfile } from '../store/actions'; | |||
import { | |||
areThereCustomOrganizations, | |||
getProjectAdminAllProfiles, | |||
getProjectAdminProjectProfiles, | |||
getComponent | |||
getProjectAdminProjectProfiles | |||
} from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -80,7 +79,6 @@ class QualityProfiles extends React.PureComponent { | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: getComponent(state, ownProps.location.query.id), | |||
customOrganizations: areThereCustomOrganizations(state), | |||
allProfiles: getProjectAdminAllProfiles(state), | |||
profiles: getProjectAdminProjectProfiles(state, ownProps.location.query.id) |
@@ -19,11 +19,9 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import PropTypes from 'prop-types'; | |||
import ProjectActivityApp from './ProjectActivityApp'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { getAllTimeMachineData } from '../../../api/time-machine'; | |||
import { getMetrics } from '../../../api/metrics'; | |||
import * as api from '../../../api/projectActivity'; | |||
@@ -45,15 +43,11 @@ import { | |||
/*:: | |||
type Props = { | |||
location: { pathname: string, query: RawQuery }, | |||
project: { | |||
component: { | |||
configuration?: { showHistory: boolean }, | |||
key: string, | |||
leakPeriodDate: string, | |||
qualifier: string | |||
}, | |||
router: { | |||
push: ({ pathname: string, query?: RawQuery }) => void, | |||
replace: ({ pathname: string, query?: RawQuery }) => void | |||
} | |||
}; | |||
*/ | |||
@@ -71,11 +65,15 @@ export type State = { | |||
}; | |||
*/ | |||
class ProjectActivityAppContainer extends React.PureComponent { | |||
export default class ProjectActivityAppContainer extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props /*: Props */) { | |||
super(props); | |||
this.state = { | |||
@@ -93,7 +91,7 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
if (isCustomGraph(newQuery.graph)) { | |||
newQuery.customMetrics = getCustomGraph(); | |||
} | |||
this.props.router.replace({ | |||
this.context.router.replace({ | |||
pathname: props.location.pathname, | |||
query: serializeUrlQuery(newQuery) | |||
}); | |||
@@ -182,7 +180,7 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
if (metrics.length <= 0) { | |||
return Promise.resolve([]); | |||
} | |||
return getAllTimeMachineData(this.props.project.key, metrics).then( | |||
return getAllTimeMachineData(this.props.component.key, metrics).then( | |||
({ measures }) => | |||
measures.map(measure => ({ | |||
metric: measure.metric, | |||
@@ -279,11 +277,11 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
...this.state.query, | |||
...newQuery | |||
}); | |||
this.props.router.push({ | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...query, | |||
id: this.props.project.key | |||
id: this.props.component.key | |||
} | |||
}); | |||
}; | |||
@@ -319,16 +317,10 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
initializing={!this.state.initialized} | |||
metrics={this.state.metrics} | |||
measuresHistory={this.state.measuresHistory} | |||
project={this.props.project} | |||
project={this.props.component} | |||
query={this.state.query} | |||
updateQuery={this.updateQuery} | |||
/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(withRouter(ProjectActivityAppContainer)); |
@@ -23,7 +23,7 @@ const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/ProjectActivityAppContainer').then(i => | |||
callback(null, { component: i.default }) | |||
callback(null, { component: (i as any).default }) | |||
); | |||
} | |||
} |
@@ -20,12 +20,9 @@ | |||
import { connect } from 'react-redux'; | |||
import App from './App'; | |||
import { fetchSettings } from '../store/actions'; | |||
import { getComponent, getSettingsAppDefaultCategory } from '../../../store/rootReducer'; | |||
import { getSettingsAppDefaultCategory } from '../../../store/rootReducer'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: ownProps.location.query.id | |||
? getComponent(state, ownProps.location.query.id) | |||
: undefined, | |||
const mapStateToProps = state => ({ | |||
defaultCategory: getSettingsAppDefaultCategory(state) | |||
}); | |||
@@ -54,15 +54,16 @@ import './styles.css'; | |||
/*:: | |||
type Props = { | |||
aroundLine?: number, | |||
branch?: string, | |||
component: string, | |||
displayAllIssues: boolean, | |||
filterLine?: (line: SourceLine) => boolean, | |||
highlightedLine?: number, | |||
highlightedLocations?: Array<FlowLocation>, | |||
highlightedLocationMessage?: { index: number, text: string }, | |||
loadComponent: string => Promise<*>, | |||
loadIssues: (string, number, number) => Promise<*>, | |||
loadSources: (string, number, number) => Promise<*>, | |||
loadComponent: (component: string, branch?: string) => Promise<*>, | |||
loadIssues: (component: string, from: number, to: number, branch?: string) => Promise<*>, | |||
loadSources: (component: string, from: number, to: number, branch?: string) => Promise<*>, | |||
onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, | |||
onLocationSelect?: number => void, | |||
onIssueChange?: Issue => void, | |||
@@ -112,16 +113,17 @@ type State = { | |||
const LINES = 500; | |||
function loadComponent(key /*: string */) /*: Promise<*> */ { | |||
return getComponentForSourceViewer(key); | |||
function loadComponent(key /*: string */, branch /*: string | void */) /*: Promise<*> */ { | |||
return getComponentForSourceViewer(key, branch); | |||
} | |||
function loadSources( | |||
key /*: string */, | |||
from /*: ?number */, | |||
to /*: ?number */ | |||
to /*: ?number */, | |||
branch /*: string | void */ | |||
) /*: Promise<Array<*>> */ { | |||
return getSources(key, from, to); | |||
return getSources(key, from, to, branch); | |||
} | |||
export default class SourceViewerBase extends React.PureComponent { | |||
@@ -175,7 +177,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
} | |||
componentDidUpdate(prevProps /*: Props */) { | |||
if (prevProps.component !== this.props.component) { | |||
if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { | |||
this.fetchComponent(); | |||
} else if ( | |||
this.props.aroundLine != null && | |||
@@ -227,7 +229,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
fetchComponent() { | |||
this.setState({ loading: true }); | |||
const loadIssues = (component, sources) => { | |||
this.props.loadIssues(this.props.component, 1, LINES).then(issues => { | |||
this.props.loadIssues(this.props.component, 1, LINES, this.props.branch).then(issues => { | |||
if (this.mounted) { | |||
const finalSources = sources.slice(0, LINES); | |||
this.setState( | |||
@@ -278,7 +280,9 @@ export default class SourceViewerBase extends React.PureComponent { | |||
); | |||
}; | |||
this.props.loadComponent(this.props.component).then(onResolve, onFailLoadComponent); | |||
this.props | |||
.loadComponent(this.props.component, this.props.branch) | |||
.then(onResolve, onFailLoadComponent); | |||
} | |||
fetchSources() { | |||
@@ -344,7 +348,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
to++; | |||
return this.props | |||
.loadSources(this.props.component, from, to) | |||
.loadSources(this.props.component, from, to, this.props.branch) | |||
.then(sources => resolve(sources), onFailLoadSources); | |||
}); | |||
} | |||
@@ -356,23 +360,25 @@ export default class SourceViewerBase extends React.PureComponent { | |||
const firstSourceLine = this.state.sources[0]; | |||
this.setState({ loadingSourcesBefore: true }); | |||
const from = Math.max(1, firstSourceLine.line - LINES); | |||
this.props.loadSources(this.props.component, from, firstSourceLine.line - 1).then(sources => { | |||
this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => { | |||
if (this.mounted) { | |||
this.setState(prevState => { | |||
const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key); | |||
return { | |||
issues: nextIssues, | |||
issuesByLine: issuesByLine(nextIssues), | |||
issueLocationsByLine: locationsByLine(nextIssues), | |||
loadingSourcesBefore: false, | |||
sources: [...this.computeCoverageStatus(sources), ...prevState.sources], | |||
symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } | |||
}; | |||
}); | |||
} | |||
this.props | |||
.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branch) | |||
.then(sources => { | |||
this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => { | |||
if (this.mounted) { | |||
this.setState(prevState => { | |||
const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key); | |||
return { | |||
issues: nextIssues, | |||
issuesByLine: issuesByLine(nextIssues), | |||
issueLocationsByLine: locationsByLine(nextIssues), | |||
loadingSourcesBefore: false, | |||
sources: [...this.computeCoverageStatus(sources), ...prevState.sources], | |||
symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } | |||
}; | |||
}); | |||
} | |||
}); | |||
}); | |||
}); | |||
}; | |||
loadSourcesAfter = () => { | |||
@@ -384,30 +390,32 @@ export default class SourceViewerBase extends React.PureComponent { | |||
const fromLine = lastSourceLine.line + 1; | |||
// request one additional line to define `hasSourcesAfter` | |||
const toLine = lastSourceLine.line + LINES + 1; | |||
this.props.loadSources(this.props.component, fromLine, toLine).then(sources => { | |||
this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => { | |||
if (this.mounted) { | |||
this.setState(prevState => { | |||
const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key); | |||
return { | |||
issues: nextIssues, | |||
issuesByLine: issuesByLine(nextIssues), | |||
issueLocationsByLine: locationsByLine(nextIssues), | |||
hasSourcesAfter: sources.length > LINES, | |||
loadingSourcesAfter: false, | |||
sources: [ | |||
...prevState.sources, | |||
...this.computeCoverageStatus(sources.slice(0, LINES)) | |||
], | |||
symbolsByLine: { | |||
...prevState.symbolsByLine, | |||
...symbolsByLine(sources.slice(0, LINES)) | |||
} | |||
}; | |||
}); | |||
} | |||
this.props | |||
.loadSources(this.props.component, fromLine, toLine, this.props.branch) | |||
.then(sources => { | |||
this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => { | |||
if (this.mounted) { | |||
this.setState(prevState => { | |||
const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key); | |||
return { | |||
issues: nextIssues, | |||
issuesByLine: issuesByLine(nextIssues), | |||
issueLocationsByLine: locationsByLine(nextIssues), | |||
hasSourcesAfter: sources.length > LINES, | |||
loadingSourcesAfter: false, | |||
sources: [ | |||
...prevState.sources, | |||
...this.computeCoverageStatus(sources.slice(0, LINES)) | |||
], | |||
symbolsByLine: { | |||
...prevState.symbolsByLine, | |||
...symbolsByLine(sources.slice(0, LINES)) | |||
} | |||
}; | |||
}); | |||
} | |||
}); | |||
}); | |||
}); | |||
}; | |||
loadDuplications = (line /*: SourceLine */) => { |
@@ -22,7 +22,7 @@ import { searchIssues } from '../../../api/issues'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
/*:: | |||
export type Query = { [string]: string }; | |||
export type Query = { [string]: string | void }; | |||
*/ | |||
/*:: | |||
@@ -31,11 +31,12 @@ export type Issues = Array<*>; */ | |||
// maximum possible value | |||
const PAGE_SIZE = 500; | |||
function buildQuery(component /*: string */) /*: Query */ { | |||
function buildQuery(component /*: string */, branch /*: string | void */) /*: Query */ { | |||
return { | |||
additionalFields: '_all', | |||
resolved: 'false', | |||
componentKeys: component, | |||
branch, | |||
s: 'FILE_LINE' | |||
}; | |||
} | |||
@@ -80,17 +81,16 @@ export function loadPageAndNext( | |||
}); | |||
} | |||
function loadIssues( | |||
export default function loadIssues( | |||
component /*: string */, | |||
fromLine /*: number */, | |||
toLine /*: number */ | |||
toLine /*: number */, | |||
branch /*: string | void */ | |||
) /*: Promise<Issues> */ { | |||
const query = buildQuery(component); | |||
const query = buildQuery(component, branch); | |||
return new Promise(resolve => { | |||
loadPageAndNext(query, toLine, 1).then(issues => { | |||
resolve(issues); | |||
}); | |||
}); | |||
} | |||
export default loadIssues; |
@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
interface Props { | |||
className?: string; | |||
color?: string; | |||
size?: number; | |||
} | |||
export default function BranchIcon({ className, color = '#4b9fd5', size = 14 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<g transform="matrix(0.0416667,0,0,0.0416667,2.98284,-1.32102)"> | |||
<path | |||
d="M72,368C72,361.333 69.667,355.667 65,351C60.333,346.333 54.667,344 48,344C41.333,344 35.667,346.333 31,351C26.333,355.667 24,361.333 24,368C24,374.667 26.333,380.333 31,385C35.667,389.667 41.333,392 48,392C54.667,392 60.333,389.667 65,385C69.667,380.333 72,374.667 72,368ZM72,80C72,73.333 69.667,67.667 65,63C60.333,58.333 54.667,56 48,56C41.333,56 35.667,58.333 31,63C26.333,67.667 24,73.333 24,80C24,86.667 26.333,92.333 31,97C35.667,101.667 41.333,104 48,104C54.667,104 60.333,101.667 65,97C69.667,92.333 72,86.667 72,80ZM232,112C232,105.333 229.667,99.667 225,95C220.333,90.333 214.667,88 208,88C201.333,88 195.667,90.333 191,95C186.333,99.667 184,105.333 184,112C184,118.667 186.333,124.333 191,129C195.667,133.667 201.333,136 208,136C214.667,136 220.333,133.667 225,129C229.667,124.333 232,118.667 232,112ZM256,112C256,120.667 253.833,128.708 249.5,136.125C245.167,143.542 239.333,149.333 232,153.5C231.667,201.333 212.833,235.833 175.5,257C164.167,263.333 147.25,270.083 124.75,277.25C103.417,283.917 89.292,289.833 82.375,295C75.458,300.167 72,308.5 72,320L72,326.5C79.333,330.667 85.167,336.458 89.5,343.875C93.833,351.292 96,359.333 96,368C96,381.333 91.333,392.667 82,402C72.667,411.333 61.333,416 48,416C34.667,416 23.333,411.333 14,402C4.667,392.667 0,381.333 0,368C0,359.333 2.167,351.292 6.5,343.875C10.833,336.458 16.667,330.667 24,326.5L24,121.5C16.667,117.333 10.833,111.542 6.5,104.125C2.167,96.708 0,88.667 0,80C0,66.667 4.667,55.333 14,46C23.333,36.667 34.667,32 48,32C61.333,32 72.667,36.667 82,46C91.333,55.333 96,66.667 96,80C96,88.667 93.833,96.708 89.5,104.125C85.167,111.542 79.333,117.333 72,121.5L72,245.75C81,241.417 93.833,236.667 110.5,231.5C119.667,228.667 126.958,226.208 132.375,224.125C137.792,222.042 143.667,219.458 150,216.375C156.333,213.292 161.25,210 164.75,206.5C168.25,203 171.625,198.75 174.875,193.75C178.125,188.75 180.458,182.958 181.875,176.375C183.292,169.792 184,162.167 184,153.5C176.667,149.333 170.833,143.542 166.5,136.125C162.167,128.708 160,120.667 160,112C160,98.667 164.667,87.333 174,78C183.333,68.667 194.667,64 208,64C221.333,64 232.667,68.667 242,78C251.333,87.333 256,98.667 256,112Z" | |||
style={{ fill: color, fillRule: 'nonzero' }} | |||
/> | |||
</g> | |||
</svg> | |||
); | |||
} |
@@ -0,0 +1,31 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
export default function PendingIcon() { | |||
/* eslint max-len: 0 */ | |||
return ( | |||
<svg width="16" height="16" className="icon-pending"> | |||
<g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)"> | |||
<path d="M224,136L224,248C224,250.333 223.25,252.25 221.75,253.75C220.25,255.25 218.333,256 216,256L136,256C133.667,256 131.75,255.25 130.25,253.75C128.75,252.25 128,250.333 128,248L128,232C128,229.667 128.75,227.75 130.25,226.25C131.75,224.75 133.667,224 136,224L192,224L192,136C192,133.667 192.75,131.75 194.25,130.25C195.75,128.75 197.667,128 200,128L216,128C218.333,128 220.25,128.75 221.75,130.25C223.25,131.75 224,133.667 224,136ZM328,224C328,199.333 321.917,176.583 309.75,155.75C297.583,134.917 281.083,118.417 260.25,106.25C239.417,94.083 216.667,88 192,88C167.333,88 144.583,94.083 123.75,106.25C102.917,118.417 86.417,134.917 74.25,155.75C62.083,176.583 56,199.333 56,224C56,248.667 62.083,271.417 74.25,292.25C86.417,313.083 102.917,329.583 123.75,341.75C144.583,353.917 167.333,360 192,360C216.667,360 239.417,353.917 260.25,341.75C281.083,329.583 297.583,313.083 309.75,292.25C321.917,271.417 328,248.667 328,224ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z" /> | |||
</g> | |||
</svg> | |||
); | |||
} |
@@ -10,6 +10,7 @@ | |||
} | |||
.navbar-context-header { | |||
float: left; | |||
line-height: 30px; | |||
font-size: 15px; | |||
} |
@@ -17,19 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import NavBar from './NavBar'; | |||
import './ContextNavBar.css'; | |||
/*:: | |||
type Props = { | |||
className?: string, | |||
height: number | |||
}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
height: number; | |||
[attr: string]: any; | |||
} | |||
export default function ContextNavBar({ className, ...other } /*: Props */) { | |||
export default function ContextNavBar({ className, ...other }: Props) { | |||
return <NavBar className={classNames('navbar-context', className)} {...other} />; | |||
} |
@@ -17,20 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import './NavBar.css'; | |||
/*:: | |||
type Props = { | |||
children?: React.Element<*>, | |||
className?: string, | |||
height: number | |||
}; | |||
*/ | |||
interface Props { | |||
children?: any; | |||
className?: string; | |||
height: number; | |||
} | |||
export default function NavBar({ children, className, height, ...other } /*: Props */) { | |||
export default function NavBar({ children, className, height, ...other }: Props) { | |||
return ( | |||
<nav {...other} className={classNames('navbar', className)} style={{ height }}> | |||
<div className="navbar-inner" style={{ height }}> |
@@ -1,6 +1,7 @@ | |||
.navbar-tabs { | |||
display: flex; | |||
align-items: center; | |||
clear: left; | |||
} | |||
.navbar-tabs > li + li { |
@@ -17,19 +17,17 @@ | |||
* 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 * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import './NavBarTabs.css'; | |||
/*:: | |||
type Props = { | |||
children?: React.Element<*>, | |||
className?: string | |||
}; | |||
*/ | |||
interface Props { | |||
children?: any; | |||
className?: string; | |||
[attr: string]: any; | |||
} | |||
export default function NavBarTabs({ children, className, ...other } /*: Props */) { | |||
export default function NavBarTabs({ children, className, ...other }: Props) { | |||
return ( | |||
<ul {...other} className={classNames('navbar-tabs', className)}> | |||
{children} |
@@ -1,33 +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. | |||
*/ | |||
import React from 'react'; | |||
export default class PendingIcon extends React.PureComponent { | |||
render() { | |||
/* eslint max-len: 0 */ | |||
return ( | |||
<svg width="16" height="16" className="icon-pending"> | |||
<g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)"> | |||
<path d="M224,136L224,248C224,250.333 223.25,252.25 221.75,253.75C220.25,255.25 218.333,256 216,256L136,256C133.667,256 131.75,255.25 130.25,253.75C128.75,252.25 128,250.333 128,248L128,232C128,229.667 128.75,227.75 130.25,226.25C131.75,224.75 133.667,224 136,224L192,224L192,136C192,133.667 192.75,131.75 194.25,130.25C195.75,128.75 197.667,128 200,128L216,128C218.333,128 220.25,128.75 221.75,130.25C223.25,131.75 224,133.667 224,136ZM328,224C328,199.333 321.917,176.583 309.75,155.75C297.583,134.917 281.083,118.417 260.25,106.25C239.417,94.083 216.667,88 192,88C167.333,88 144.583,94.083 123.75,106.25C102.917,118.417 86.417,134.917 74.25,155.75C62.083,176.583 56,199.333 56,224C56,248.667 62.083,271.417 74.25,292.25C86.417,313.083 102.917,329.583 123.75,341.75C144.583,353.917 167.333,360 192,360C216.667,360 239.417,353.917 260.25,341.75C281.083,329.583 297.583,313.083 309.75,292.25C321.917,271.417 328,248.667 328,224ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z" /> | |||
</g> | |||
</svg> | |||
); | |||
} | |||
} |
@@ -49,13 +49,14 @@ export default BaseView.extend({ | |||
}, | |||
showViewer() { | |||
const { key, line } = this.model.toJSON(); | |||
const { branch, key, line } = this.model.toJSON(); | |||
const el = document.querySelector(this.viewerRegion.el); | |||
render( | |||
<WithStore> | |||
<SourceViewer | |||
branch={branch} | |||
component={key} | |||
fromWorkspace={true} | |||
highlightedLine={line} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 { Branch, BranchType, ShortLivingBranch } from '../app/types'; | |||
export const MAIN_BRANCH: Branch = { | |||
isMain: true, | |||
name: undefined, | |||
type: BranchType.LONG | |||
}; | |||
const MAIN_BRANCH_DISPLAY_NAME = 'master'; | |||
export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch { | |||
return branch != null && branch.type === BranchType.SHORT; | |||
} | |||
export function getBranchDisplayName(branch: Branch): string { | |||
return branch.isMain ? MAIN_BRANCH_DISPLAY_NAME : branch.name; | |||
} |
@@ -18,7 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { stringify } from 'querystring'; | |||
import { isShortLivingBranch } from './branches'; | |||
import { getProfilePath } from '../apps/quality-profiles/utils'; | |||
import { Branch } from '../app/types'; | |||
interface Query { | |||
[x: string]: string; | |||
@@ -40,6 +42,17 @@ export function getProjectUrl(key: string): Location { | |||
return { pathname: '/dashboard', query: { id: key } }; | |||
} | |||
export function getProjectBranchUrl(key: string, branch: Branch) { | |||
if (isShortLivingBranch(branch)) { | |||
return { | |||
pathname: '/project/issues', | |||
query: { branch: branch.name, id: key, resolved: 'false' } | |||
}; | |||
} else { | |||
return { pathname: '/dashboard', query: { id: key } }; | |||
} | |||
} | |||
/** | |||
* Generate URL for a global issues page | |||
*/ |
@@ -18,13 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getLanguages } from '../api/languages'; | |||
import { getGlobalNavigation, getComponentNavigation } from '../api/nav'; | |||
import { getComponentData } from '../api/components'; | |||
import { getGlobalNavigation } from '../api/nav'; | |||
import * as auth from '../api/auth'; | |||
import { getOrganizations } from '../api/organizations'; | |||
import { getMetrics } from '../api/metrics'; | |||
import { receiveLanguages } from './languages/actions'; | |||
import { receiveComponents } from './components/actions'; | |||
import { receiveMetrics } from './metrics/actions'; | |||
import { addGlobalErrorMessage } from './globalMessages/duck'; | |||
import { parseError } from '../apps/code/utils'; | |||
@@ -49,23 +47,6 @@ export const fetchOrganizations = (organizations /*: Array<string> | void */) => | |||
onFail(dispatch) | |||
); | |||
const addQualifier = project => ({ | |||
...project, | |||
qualifier: project.breadcrumbs[project.breadcrumbs.length - 1].qualifier | |||
}); | |||
export const fetchProject = key => dispatch => | |||
Promise.all([ | |||
getComponentNavigation(key), | |||
getComponentData(key) | |||
]).then(([componentNav, componentData]) => { | |||
const component = { ...componentData, ...componentNav }; | |||
dispatch(receiveComponents([addQualifier(component)])); | |||
if (component.organization != null) { | |||
dispatch(fetchOrganizations([component.organization])); | |||
} | |||
}); | |||
export const doLogin = (login, password) => dispatch => | |||
auth.login(login, password).then( | |||
() => { |
@@ -68,6 +68,10 @@ | |||
right: auto; | |||
} | |||
.dropdown-menu-shadow { | |||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175); | |||
} | |||
.dropdown-header { | |||
display: block; | |||
padding: 3px 8px 5px; |
@@ -135,3 +135,9 @@ | |||
} | |||
} | |||
} | |||
.menu-message { | |||
display: block; | |||
padding: 4px 16px; | |||
line-height: 16px; | |||
} |