Browse Source

SONAR-9702 Build UI for short-lived branches (#2371)

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
cff416d7f9
82 changed files with 1901 additions and 524 deletions
  1. 11
    7
      server/sonar-web/src/main/js/api/branches.ts
  2. 21
    12
      server/sonar-web/src/main/js/api/components.ts
  3. 4
    3
      server/sonar-web/src/main/js/api/nav.ts
  4. 6
    12
      server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
  5. 0
    104
      server/sonar-web/src/main/js/app/components/ProjectContainer.js
  6. 143
    0
      server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
  7. 41
    0
      server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
  8. 1
    2
      server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx
  9. 1
    6
      server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js
  10. 13
    26
      server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
  11. 18
    0
      server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css
  12. 73
    0
      server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx
  13. 17
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
  14. 32
    11
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  15. 84
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  16. 222
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  17. 64
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
  18. 36
    11
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  19. 31
    9
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  20. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx
  21. 44
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx
  22. 50
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  23. 92
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  24. 58
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
  25. 12
    3
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
  26. 7
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
  27. 91
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  28. 41
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
  29. 157
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  30. 90
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
  31. 4
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  32. 0
    1
      server/sonar-web/src/main/js/app/components/search/Search.css
  33. 1
    1
      server/sonar-web/src/main/js/app/components/search/Search.js
  34. 1
    8
      server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
  35. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js
  36. 12
    24
      server/sonar-web/src/main/js/apps/code/components/App.js
  37. 2
    2
      server/sonar-web/src/main/js/apps/code/components/Component.js
  38. 2
    2
      server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
  39. 1
    1
      server/sonar-web/src/main/js/apps/code/components/ComponentName.js
  40. 2
    2
      server/sonar-web/src/main/js/apps/code/components/ComponentPin.js
  41. 10
    4
      server/sonar-web/src/main/js/apps/code/components/Search.js
  42. 1
    1
      server/sonar-web/src/main/js/apps/code/routes.ts
  43. 12
    12
      server/sonar-web/src/main/js/apps/code/utils.js
  44. 2
    8
      server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
  45. 1
    9
      server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
  46. 1
    1
      server/sonar-web/src/main/js/apps/custom-measures/routes.ts
  47. 11
    4
      server/sonar-web/src/main/js/apps/issues/components/App.js
  48. 2
    5
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
  49. 4
    0
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
  50. 4
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
  51. 9
    2
      server/sonar-web/src/main/js/apps/overview/components/App.js
  52. 8
    2
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
  53. 9
    2
      server/sonar-web/src/main/js/apps/overview/meta/Meta.js
  54. 13
    3
      server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
  55. 4
    8
      server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
  56. 61
    0
      server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js
  57. 2
    1
      server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
  58. 1
    1
      server/sonar-web/src/main/js/apps/overview/routes.ts
  59. 8
    27
      server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js
  60. 2
    3
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  61. 2
    3
      server/sonar-web/src/main/js/apps/project-admin/links/Links.js
  62. 2
    7
      server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
  63. 1
    3
      server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
  64. 12
    20
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  65. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/routes.ts
  66. 2
    5
      server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
  67. 58
    50
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
  68. 7
    7
      server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
  69. 45
    0
      server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
  70. 31
    0
      server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx
  71. 1
    0
      server/sonar-web/src/main/js/components/nav/ContextNavBar.css
  72. 8
    10
      server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx
  73. 8
    11
      server/sonar-web/src/main/js/components/nav/NavBar.tsx
  74. 1
    0
      server/sonar-web/src/main/js/components/nav/NavBarTabs.css
  75. 8
    10
      server/sonar-web/src/main/js/components/nav/NavBarTabs.tsx
  76. 0
    33
      server/sonar-web/src/main/js/components/shared/pending-icon.js
  77. 2
    1
      server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
  78. 36
    0
      server/sonar-web/src/main/js/helpers/branches.ts
  79. 13
    0
      server/sonar-web/src/main/js/helpers/urls.ts
  80. 1
    20
      server/sonar-web/src/main/js/store/rootActions.js
  81. 4
    0
      server/sonar-web/src/main/less/components/dropdowns.less
  82. 6
    0
      server/sonar-web/src/main/less/components/menu.less

server/sonar-web/src/main/js/apps/overview/components/AppContainer.js → server/sonar-web/src/main/js/api/branches.ts View File

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

+ 21
- 12
server/sonar-web/src/main/js/api/components.ts View File

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

+ 4
- 3
server/sonar-web/src/main/js/api/nav.ts View File

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

+ 6
- 12
server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js View File

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

+ 0
- 104
server/sonar-web/src/main/js/app/components/ProjectContainer.js View File

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

+ 143
- 0
server/sonar-web/src/main/js/app/components/ProjectContainer.tsx View File

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

+ 41
- 0
server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx View File

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

server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js → server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx View File

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

+ 1
- 6
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js View File

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

server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js → server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx View File

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

+ 18
- 0
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css View File

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

+ 73
- 0
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx View File

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

+ 17
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css View File

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

server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

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

+ 84
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx View File

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

+ 222
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx View File

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

+ 64
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx View File

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

server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

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

server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx View File

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

server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js → server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx View File

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


+ 44
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx View File

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

+ 50
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx View File

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

+ 92
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx View File

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

+ 58
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx View File

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

server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js → server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx View File

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

+ 7
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx View File

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

+ 91
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap View File

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

+ 41
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap View File

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

+ 157
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap View File

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

+ 90
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap View File

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

server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap → server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -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",
},
}

+ 0
- 1
server/sonar-web/src/main/js/app/components/search/Search.css View File

@@ -93,5 +93,4 @@
padding: 0;
overflow-y: auto;
overflow-x: hidden;
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}

+ 1
- 1
server/sonar-web/src/main/js/app/components/search/Search.js View File

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

+ 1
- 8
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js View File

@@ -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'; */


+ 12
- 24
server/sonar-web/src/main/js/apps/code/components/App.js View File

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

+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/Component.js View File

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


+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js View File

@@ -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')}
/>

+ 1
- 1
server/sonar-web/src/main/js/apps/code/components/ComponentName.js View File

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

+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/ComponentPin.js View File

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

+ 10
- 4
server/sonar-web/src/main/js/apps/code/components/Search.js View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/code/routes.ts View File

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

+ 12
- 12
server/sonar-web/src/main/js/apps/code/utils.js View File

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

+ 2
- 8
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js View File

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

+ 1
- 9
server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/custom-measures/routes.ts View File

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

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

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

+ 2
- 5
server/sonar-web/src/main/js/apps/issues/components/AppContainer.js View File

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


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

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

+ 4
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js View File

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

+ 9
- 2
server/sonar-web/src/main/js/apps/overview/components/App.js View File

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

+ 8
- 2
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js View File

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

+ 9
- 2
server/sonar-web/src/main/js/apps/overview/meta/Meta.js View File

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

+ 13
- 3
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js View File

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

server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js → server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js View File

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

+ 61
- 0
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js View File

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

+ 2
- 1
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/routes.ts View File

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

+ 8
- 27
server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js View File

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

+ 2
- 3
server/sonar-web/src/main/js/apps/project-admin/key/Key.js View File

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


+ 2
- 3
server/sonar-web/src/main/js/apps/project-admin/links/Links.js View File

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


+ 2
- 7
server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js View File

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

+ 1
- 3
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js View File

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

+ 12
- 20
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/routes.ts View File

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

+ 2
- 5
server/sonar-web/src/main/js/apps/settings/components/AppContainer.js View File

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


+ 58
- 50
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js View File

@@ -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 */) => {

+ 7
- 7
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js View File

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

+ 45
- 0
server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx View File

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

+ 31
- 0
server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx View File

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

+ 1
- 0
server/sonar-web/src/main/js/components/nav/ContextNavBar.css View File

@@ -10,6 +10,7 @@
}

.navbar-context-header {
float: left;
line-height: 30px;
font-size: 15px;
}

server/sonar-web/src/main/js/components/nav/ContextNavBar.js → server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx View File

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

server/sonar-web/src/main/js/components/nav/NavBar.js → server/sonar-web/src/main/js/components/nav/NavBar.tsx View File

@@ -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
- 0
server/sonar-web/src/main/js/components/nav/NavBarTabs.css View File

@@ -1,6 +1,7 @@
.navbar-tabs {
display: flex;
align-items: center;
clear: left;
}

.navbar-tabs > li + li {

server/sonar-web/src/main/js/components/nav/NavBarTabs.js → server/sonar-web/src/main/js/components/nav/NavBarTabs.tsx View File

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

+ 0
- 33
server/sonar-web/src/main/js/components/shared/pending-icon.js View File

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

+ 2
- 1
server/sonar-web/src/main/js/components/workspace/views/viewer-view.js View File

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

+ 36
- 0
server/sonar-web/src/main/js/helpers/branches.ts View File

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

+ 13
- 0
server/sonar-web/src/main/js/helpers/urls.ts View File

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

+ 1
- 20
server/sonar-web/src/main/js/store/rootActions.js View File

@@ -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(
() => {

+ 4
- 0
server/sonar-web/src/main/less/components/dropdowns.less View File

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

+ 6
- 0
server/sonar-web/src/main/less/components/menu.less View File

@@ -135,3 +135,9 @@
}
}
}

.menu-message {
display: block;
padding: 4px 16px;
line-height: 16px;
}

Loading…
Cancel
Save