From cff416d7f9910c258bc8d7175c08afff96a9eb2a Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 17 Aug 2017 21:39:59 +0200 Subject: [PATCH] SONAR-9702 Build UI for short-lived branches (#2371) --- .../AppContainer.js => api/branches.ts} | 18 +- .../sonar-web/src/main/js/api/components.ts | 33 ++- server/sonar-web/src/main/js/api/nav.ts | 7 +- .../app/components/ProjectAdminContainer.js | 18 +- .../js/app/components/ProjectContainer.js | 104 -------- .../js/app/components/ProjectContainer.tsx | 143 +++++++++++ .../__tests__/ProjectContainer-test.tsx | 41 ++++ ...nsionNotFound.js => ExtensionNotFound.tsx} | 3 +- .../extensions/ProjectAdminPageExtension.js | 7 +- ...eExtension.js => ProjectPageExtension.tsx} | 39 +-- .../components/nav/component/BranchStatus.css | 18 ++ .../components/nav/component/BranchStatus.tsx | 73 ++++++ .../components/nav/component/ComponentNav.css | 17 ++ .../{ComponentNav.js => ComponentNav.tsx} | 43 +++- .../nav/component/ComponentNavBranch.tsx | 84 +++++++ .../component/ComponentNavBranchesMenu.tsx | 222 ++++++++++++++++++ .../ComponentNavBranchesMenuItem.tsx | 64 +++++ ...mponentNavMenu.js => ComponentNavMenu.tsx} | 47 +++- ...mponentNavMeta.js => ComponentNavMeta.tsx} | 40 +++- ...crementalBadge.js => IncrementalBadge.tsx} | 2 +- .../component/__tests__/BranchStatus-test.tsx | 44 ++++ .../__tests__/ComponentNavBranch-test.tsx | 50 ++++ .../ComponentNavBranchesMenu-test.tsx | 92 ++++++++ .../ComponentNavBranchesMenuItem-test.tsx | 58 +++++ ...Menu-test.js => ComponentNavMenu-test.tsx} | 15 +- .../__tests__/ComponentNavMeta-test.tsx | 8 +- .../__snapshots__/BranchStatus-test.tsx.snap | 91 +++++++ .../ComponentNavBranch-test.tsx.snap | 41 ++++ .../ComponentNavBranchesMenu-test.tsx.snap | 157 +++++++++++++ ...ComponentNavBranchesMenuItem-test.tsx.snap | 90 +++++++ ...js.snap => ComponentNavMenu-test.tsx.snap} | 4 + .../main/js/app/components/search/Search.css | 1 - .../main/js/app/components/search/Search.js | 2 +- .../components/BackgroundTasksApp.js | 9 +- .../background-tasks/components/TaskStatus.js | 2 +- .../src/main/js/apps/code/components/App.js | 36 +-- .../main/js/apps/code/components/Component.js | 4 +- .../apps/code/components/ComponentDetach.js | 4 +- .../js/apps/code/components/ComponentName.js | 2 +- .../js/apps/code/components/ComponentPin.js | 4 +- .../main/js/apps/code/components/Search.js | 14 +- .../sonar-web/src/main/js/apps/code/routes.ts | 2 +- .../sonar-web/src/main/js/apps/code/utils.js | 24 +- .../components/AppContainer.js | 10 +- .../components/CustomMeasuresAppContainer.js | 10 +- .../main/js/apps/custom-measures/routes.ts | 2 +- .../src/main/js/apps/issues/components/App.js | 15 +- .../js/apps/issues/components/AppContainer.js | 7 +- .../issues/components/IssuesSourceViewer.js | 4 + .../apps/issues/sidebar/CreationDateFacet.js | 4 + .../main/js/apps/overview/components/App.js | 11 +- .../apps/overview/components/OverviewApp.js | 10 +- .../src/main/js/apps/overview/meta/Meta.js | 11 +- .../main/js/apps/overview/meta/MetaTags.js | 16 +- .../meta/MetaTagsSelector.js} | 12 +- .../meta/__tests__/MetaTagsSelector-test.js | 61 +++++ .../__snapshots__/MetaTags-test.js.snap | 3 +- .../src/main/js/apps/overview/routes.ts | 2 +- .../apps/project-admin/deletion/Deletion.js | 35 +-- .../src/main/js/apps/project-admin/key/Key.js | 5 +- .../main/js/apps/project-admin/links/Links.js | 5 +- .../project-admin/quality-gate/QualityGate.js | 9 +- .../quality-profiles/QualityProfiles.js | 4 +- .../components/ProjectActivityAppContainer.js | 32 +-- .../main/js/apps/projectActivity/routes.ts | 2 +- .../apps/settings/components/AppContainer.js | 7 +- .../SourceViewer/SourceViewerBase.js | 108 +++++---- .../SourceViewer/helpers/loadIssues.js | 14 +- .../icons-components/BranchIcon.tsx | 45 ++++ .../icons-components/PendingIcon.tsx | 31 +++ .../main/js/components/nav/ContextNavBar.css | 1 + .../{ContextNavBar.js => ContextNavBar.tsx} | 18 +- .../components/nav/{NavBar.js => NavBar.tsx} | 19 +- .../src/main/js/components/nav/NavBarTabs.css | 1 + .../nav/{NavBarTabs.js => NavBarTabs.tsx} | 18 +- .../main/js/components/shared/pending-icon.js | 33 --- .../components/workspace/views/viewer-view.js | 3 +- .../sonar-web/src/main/js/helpers/branches.ts | 36 +++ server/sonar-web/src/main/js/helpers/urls.ts | 13 + .../src/main/js/store/rootActions.js | 21 +- .../src/main/less/components/dropdowns.less | 4 + .../src/main/less/components/menu.less | 6 + 82 files changed, 1901 insertions(+), 524 deletions(-) rename server/sonar-web/src/main/js/{apps/overview/components/AppContainer.js => api/branches.ts} (63%) delete mode 100644 server/sonar-web/src/main/js/app/components/ProjectContainer.js create mode 100644 server/sonar-web/src/main/js/app/components/ProjectContainer.tsx create mode 100644 server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx rename server/sonar-web/src/main/js/app/components/extensions/{ExtensionNotFound.js => ExtensionNotFound.tsx} (97%) rename server/sonar-web/src/main/js/app/components/extensions/{ProjectPageExtension.js => ProjectPageExtension.tsx} (58%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx rename server/sonar-web/src/main/js/app/components/nav/component/{ComponentNav.js => ComponentNav.tsx} (72%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx rename server/sonar-web/src/main/js/app/components/nav/component/{ComponentNavMenu.js => ComponentNavMenu.tsx} (89%) rename server/sonar-web/src/main/js/app/components/nav/component/{ComponentNavMeta.js => ComponentNavMeta.tsx} (76%) rename server/sonar-web/src/main/js/app/components/nav/component/{IncrementalBadge.js => IncrementalBadge.tsx} (97%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx rename server/sonar-web/src/main/js/app/components/nav/component/__tests__/{ComponentNavMenu-test.js => ComponentNavMenu-test.tsx} (80%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap rename server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/{ComponentNavMenu-test.js.snap => ComponentNavMenu-test.tsx.snap} (98%) rename server/sonar-web/src/main/js/apps/{projects/components/ProjectTagsSelectorContainer.js => overview/meta/MetaTagsSelector.js} (82%) create mode 100644 server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js create mode 100644 server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx create mode 100644 server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx rename server/sonar-web/src/main/js/components/nav/{ContextNavBar.js => ContextNavBar.tsx} (81%) rename server/sonar-web/src/main/js/components/nav/{NavBar.js => NavBar.tsx} (86%) rename server/sonar-web/src/main/js/components/nav/{NavBarTabs.js => NavBarTabs.tsx} (85%) delete mode 100644 server/sonar-web/src/main/js/components/shared/pending-icon.js create mode 100644 server/sonar-web/src/main/js/helpers/branches.ts diff --git a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js b/server/sonar-web/src/main/js/api/branches.ts similarity index 63% rename from server/sonar-web/src/main/js/apps/overview/components/AppContainer.js rename to server/sonar-web/src/main/js/api/branches.ts index a6025ae8fe4..5c597385c85 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -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 { + 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 { + return getJSON('/api/project_branches/show', { component: project, branch }).then( + r => r.branch, + throwGlobalError + ); +} diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 50faefec07d..fd46c34d559 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -113,8 +113,12 @@ export function getComponentLeaves( return getComponentTree('leaves', componentKey, metrics, additional); } -export function getComponent(componentKey: string, metrics: string[] = []): Promise { - const data = { componentKey, metricKeys: metrics.join(',') }; +export function getComponent( + componentKey: string, + metrics: string[] = [], + branch?: string +): Promise { + 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 { - return getJSON('/api/components/show', { component }); +export function getComponentShow(component: string, branch?: string): Promise { + return getJSON('/api/components/show', { component, branch }); } export function getParents(component: string): Promise { return getComponentShow(component).then(r => r.ancestors); } -export function getBreadcrumbs(component: string): Promise { - return getComponentShow(component).then(r => { +export function getBreadcrumbs(component: string, branch?: string): Promise { + return getComponentShow(component, branch).then(r => { const reversedAncestors = [...r.ancestors].reverse(); return [...reversedAncestors, r.component]; }); } -export function getComponentData(component: string): Promise { - return getComponentShow(component).then(r => r.component); +export function getComponentData(component: string, branch?: string): Promise { + return getComponentShow(component, branch).then(r => r.component); } export function getMyProjects(data: RequestData): Promise { @@ -219,12 +223,17 @@ export function getSuggestions( return getJSON('/api/components/suggestions', data); } -export function getComponentForSourceViewer(component: string): Promise { - return getJSON('/api/components/app', { component }); +export function getComponentForSourceViewer(component: string, branch?: string): Promise { + return getJSON('/api/components/app', { component, branch }); } -export function getSources(component: string, from?: number, to?: number): Promise { - const data: RequestData = { key: component }; +export function getSources( + component: string, + from?: number, + to?: number, + branch?: string +): Promise { + const data: RequestData = { key: component, branch }; if (from) { Object.assign(data, { from }); } diff --git a/server/sonar-web/src/main/js/api/nav.ts b/server/sonar-web/src/main/js/api/nav.ts index 3b2046df1c0..0e1983d0527 100644 --- a/server/sonar-web/src/main/js/api/nav.ts +++ b/server/sonar-web/src/main/js/api/nav.ts @@ -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 { return getJSON('/api/navigation/global'); } -export function getComponentNavigation(componentKey: string): Promise { - return getJSON('/api/navigation/component', { componentKey }); +export function getComponentNavigation(componentKey: string, branch?: string): Promise { + return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError); } export function getSettingsNavigation(): Promise { - return getJSON('/api/navigation/settings'); + return getJSON('/api/navigation/settings').catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js index 40b591a198d..fed3232e2c2 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js +++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js deleted file mode 100644 index b40bd8f9b25..00000000000 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js +++ /dev/null @@ -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 ( -
- {!isFile && - } - {/* $FlowFixMe */} - {React.cloneElement(this.props.children, { - component: project, - onComponentChange: this.handleProjectChange - })} -
- ); - } -} - -const mapStateToProps = (state, ownProps) => ({ - project: getComponent(state, ownProps.location.query.id) -}); - -const mapDispatchToProps = { addGlobalErrorMessage, fetchProject, receiveComponents }; - -export default connect(mapStateToProps, mapDispatchToProps)(ProjectContainer); diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx new file mode 100644 index 00000000000..ef19629d7e3 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx @@ -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 { + 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 ( +
+ {!isFile && + } + {React.cloneElement(this.props.children, { + branch, + component: component, + onComponentChange: this.handleProjectChange + })} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx new file mode 100644 index 00000000000..5400c85f212 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx @@ -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 = () =>
; + + const wrapper = shallow( + + + + ); + (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' }); +}); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx similarity index 97% rename from server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js rename to server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx index 330b818f931..154b674f6b9 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js +++ b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx @@ -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 { diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js index 3a432553dd8..a72607ce2d0 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js @@ -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 */) { : ; } -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx similarity index 58% rename from server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js rename to server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx index dd216b54bac..c06716a279c 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx @@ -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 ? : ; } - -const mapStateToProps = (state, ownProps /*: Props */) => ({ - component: getComponent(state, ownProps.location.query.id) -}); - -const mapDispatchToProps = { onFail: addGlobalErrorMessage }; - -export default connect(mapStateToProps, mapDispatchToProps)(ProjectPageExtension); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css new file mode 100644 index 00000000000..b52fc694772 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx new file mode 100644 index 00000000000..bac5d7e295f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx @@ -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 ( +
    +
  • + 0, + 'is-passed': totalIssues === 0 + })} + /> +
  • + {concise && +
  • + {totalIssues} +
  • } + {!concise && +
  • + {branch.status.bugs} + +
  • } + {!concise && +
  • + {branch.status.vulnerabilities} + +
  • } + {!concise && +
  • + {branch.status.codeSmells} + +
  • } +
+ ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css index 22ec0d36ea1..e29574c39e4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx similarity index 72% rename from server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 0e21abc3301..9729aea4cd6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -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 { + 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} /> + + ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx new file mode 100644 index 00000000000..1ac3fa296ec --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -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 { + 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) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.setState({ open: true }); + }; + + closeDropdown = () => { + if (this.mounted) { + this.setState({ open: false }); + } + }; + + render() { + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx new file mode 100644 index 00000000000..6efe8e25b18 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -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 { + 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) => + this.setState({ query: event.currentTarget.value, selected: null }); + + handleKeyDown = (event: React.KeyboardEvent) => { + 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 = () => +
+ + +
; + + renderBranchesList = () => { + const branches = this.getFilteredBranches(); + + const selected = this.getSelected(); + + return branches.length > 0 + ?
    + {branches.map(branch => + + )} +
+ :
+ {translate('no_results')} +
; + }; + + render() { + return ( +
(this.node = node)}> + {this.state.loading + ? + :
+ {this.renderSearch()} + {this.renderBranchesList()} +
} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx new file mode 100644 index 00000000000..44821562e28 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx @@ -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 ( +
  • + +
    + + {displayName} +
    +
    + +
    + +
  • + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx similarity index 89% rename from server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index dfbd7899d6e..15c6087e4ec 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -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 { 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 (
  • @@ -80,7 +86,10 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • {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 (
  • {translate('issues.page')} @@ -122,6 +139,10 @@ export default class ComponentNavMenu extends React.PureComponent { } renderComponentMeasuresLink() { + if (isShortLivingBranch(this.props.branch)) { + return null; + } + return (
  • 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 (
  • diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx similarity index 76% rename from server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js rename to server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index 28133dd41fe..fae37abdabd 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -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) { ); } - if (props.analysisDate) { + + if (props.component.analysisDate && props.branch.isMain) { metaList.push(
  • - +
  • ); } - if (props.version) { + if (props.component.version && props.branch.isMain) { metaList.push(
  • - Version {props.version} + Version {props.component.version}
  • ); } @@ -100,6 +114,14 @@ export default function ComponentNavMeta(props) { ); } + if (!props.branch.isMain) { + metaList.push( +
  • + +
  • + ); + } + return (
      diff --git a/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js b/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx similarity index 97% rename from server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js rename to server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx index 2692360e630..0ac87aaa448 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx @@ -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'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx new file mode 100644 index 00000000000..e932d89cc4c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx @@ -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( + + ) + ).toMatchSnapshot(); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx new file mode 100644 index 00000000000..8b138e9aa90 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -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()).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()).toMatchSnapshot(); +}); + +it('opens menu', () => { + const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; + const component = {} as Component; + const wrapper = shallow(); + expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); + click(wrapper.find('a')); + expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx new file mode 100644 index 00000000000..c6ef473552c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx @@ -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( + + ); + wrapper.setState({ + branches: [mainBranch(), shortBranch('foo'), longBranch('bar')], + loading: false + }); + expect(wrapper).toMatchSnapshot(); +}); + +it('searches', () => { + const component = { key: 'component' } as Component; + const wrapper = shallow( + + ); + 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( + + ); + 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 }; +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx new file mode 100644 index 00000000000..ba32de85805 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx @@ -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( + + ) + ).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( + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx similarity index 80% rename from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js rename to server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index d1c1da6bc25..dc3ce7d17e8 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -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()).toMatchSnapshot(); + expect( + shallow( + + ) + ).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()).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx index e5f0a6ab6fa..766d5aff111 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -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( - + ).find('IncrementalBadge') ).toHaveLength(incremental ? 1 : 0); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap new file mode 100644 index 00000000000..ab40c58e93c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
        +
      • + +
      • +
      • + 0 + +
      • +
      • + 0 + +
      • +
      • + 0 + +
      • +
      +`; + +exports[`renders 2`] = ` +
        +
      • + +
      • +
      • + 0 + +
      • +
      • + 0 + +
      • +
      • + 1 + +
      • +
      +`; + +exports[`renders 3`] = ` +
        +
      • + +
      • +
      • + 7 + +
      • +
      • + 6 + +
      • +
      • + 3 + +
      • +
      +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap new file mode 100644 index 00000000000..3ead4391274 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders main branch 1`] = ` + +`; + +exports[`renders short-living branch 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap new file mode 100644 index 00000000000..dc05a411296 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders list 1`] = ` +
      +
      +
      + + +
      +
        + + + +
      +
      +
      +`; + +exports[`searches 1`] = ` +
      +
      +
      + + +
      +
        + + +
      +
      +
      +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap new file mode 100644 index 00000000000..e579bfadf0c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders main branch 1`] = ` +
    • + +
      + + master +
      +
      + +
      + +
    • +`; + +exports[`renders short-living branch 1`] = ` +
    • + +
      + + foo +
      +
      + +
      + +
    • +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap similarity index 98% rename from server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap rename to server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 1772a834780..fac7593c0f9 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css index f9297eabb83..7cdd0a20035 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.css +++ b/server/sonar-web/src/main/js/app/components/search/Search.css @@ -93,5 +93,4 @@ padding: 0; overflow-y: auto; overflow-x: hidden; - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); } diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js index 84c35015e94..ea4592d3992 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.js +++ b/server/sonar-web/src/main/js/app/components/search/Search.js @@ -354,7 +354,7 @@ export default class Search extends React.PureComponent { {this.state.open && Object.keys(this.state.results).length > 0 &&
      (this.node = node)}> ({ - 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); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js index 5a19455b17b..b2ef518c558 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js @@ -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'; */ diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js index 3a0f994aa1c..c59cac6bade 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -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}
      } - +
      {shouldShowBreadcrumbs && @@ -202,24 +200,14 @@ class App extends React.PureComponent {
      } {shouldShowComponents && - } + } {shouldShowSourceViewer &&
      - +
      }
    ); } } - -const mapStateToProps = (state, ownProps) => ({ - component: getComponent(state, ownProps.location.query.id) -}); - -export default connect(mapStateToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js index e36ad13bcf6..4fe40dfa618 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ b/server/sonar-web/src/main/js/apps/code/components/Component.js @@ -70,10 +70,10 @@ export default class Component extends React.PureComponent { switch (component.qualifier) { case 'FIL': case 'UTS': - componentAction = ; + componentAction = ; break; default: - componentAction = ; + componentAction = ; } } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js index 5277be98427..30fccdfa7bf 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js index bad7f487537..921aa3f724c 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js @@ -71,7 +71,7 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { ); } 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 }); } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js index 7d0f9478c59..641017207d7 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js index a34d87e3c9e..4d772e7a3b1 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ b/server/sonar-web/src/main/js/apps/code/components/Search.js @@ -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 }); diff --git a/server/sonar-web/src/main/js/apps/code/routes.ts b/server/sonar-web/src/main/js/apps/code/routes.ts index fcffeff191c..9eb0b5ef7c3 100644 --- a/server/sonar-web/src/main/js/apps/code/routes.ts +++ b/server/sonar-web/src/main/js/apps/code/routes.ts @@ -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 })); } } ]; diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js index 2a1f5552cc0..52975be78ec 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.js +++ b/server/sonar-web/src/main/js/apps/code/utils.js @@ -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 => { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js index f3d91451c47..f823961bb6f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -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) diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js index 1f40c41775f..4f2f1ba1ca6 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/custom-measures/routes.ts b/server/sonar-web/src/main/js/apps/custom-measures/routes.ts index ad092ba1d48..cc0a32c95c1 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/routes.ts +++ b/server/sonar-web/src/main/js/apps/custom-measures/routes.ts @@ -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 }) ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 4a0040f2991..509a69dddce 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -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 {
    {openIssue ? ({ - component: ownProps.location.query.id - ? getComponent(state, ownProps.location.query.id) - : undefined, +const mapStateToProps = state => ({ currentUser: getCurrentUser(state) }); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js index 498f2e377ce..6561fd547fa 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -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 {
    (this.node = node)}> }, + 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 ; } - return ; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index ac67fbae194..9e28bf48386 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -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 {
    - +
    diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index fd849228480..a79b44dbd41 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -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 - {isProject && } + {isProject && } {(isProject || isApplication) && void }; */ @@ -104,6 +106,13 @@ export default class MetaTags extends React.PureComponent { }; } + handleSetProjectTags = (tags /*: Array */) => { + 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 { {popupOpen &&
    (this.tagsSelector = tagsSelector)}> -
    } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js similarity index 82% rename from server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js rename to server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js index 2cb4ab4e54a..76e25c9c1f1 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js @@ -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, - setProjectTags: (string, Array) => void + setProjectTags: (Array) => 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); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js new file mode 100644 index 00000000000..59744a204de --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js @@ -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( + + ); + jest.runAllTimers(); + + expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' }); +}); + +it('selects and deselects tags', () => { + const setProjectTags = jest.fn(); + const wrapper = shallow( + + ); + + 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']); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap index 0eb1415ab03..875302462d5 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap @@ -40,7 +40,7 @@ exports[`should open the tag selector on click 2`] = ` />
    -
    diff --git a/server/sonar-web/src/main/js/apps/overview/routes.ts b/server/sonar-web/src/main/js/apps/overview/routes.ts index 21e41c6b087..9eb0b5ef7c3 100644 --- a/server/sonar-web/src/main/js/apps/overview/routes.ts +++ b/server/sonar-web/src/main/js/apps/overview/routes.ts @@ -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 })); } } ]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js index 5aff00dd956..9cd6a47ea96 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js +++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js @@ -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 ( -
    - -
    -
    -
    - ); - } +export default function Deletion(props) { + return ( +
    + +
    + +
    + ); } - -const mapStateToProps = (state, ownProps) => ({ - component: getComponent(state, ownProps.location.query.id) -}); - -export default connect(mapStateToProps)(Deletion); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js index 130d67cd4c2..4a340ab6371 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js +++ b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js @@ -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) }); diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js index fede0404e07..592ae5fc092 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js @@ -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) }); diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js index f07fbed428b..6194c04371a 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js @@ -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) }); diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js index 4551f410a25..02ff3c3eac5 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js @@ -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) diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 1d3c9845ecd..de7cde52b7a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -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)); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts b/server/sonar-web/src/main/js/apps/projectActivity/routes.ts index 47d7bda3dae..8641ea7391c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/routes.ts @@ -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 }) ); } } diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js index 8e80ca6cb6f..cbc69d53cfa 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js @@ -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) }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index d656865b0ec..8c72221dd3c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -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, 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> */ { - 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 */) => { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js index 8d86b7c4a19..be548183d6e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js @@ -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 */ { - const query = buildQuery(component); + const query = buildQuery(component, branch); return new Promise(resolve => { loadPageAndNext(query, toLine, 1).then(issues => { resolve(issues); }); }); } - -export default loadIssues; diff --git a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx new file mode 100644 index 00000000000..420d94ebd6a --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx @@ -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 ( + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx new file mode 100644 index 00000000000..74140a325e7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx @@ -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 ( + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css index ebebf69464a..25f9470753a 100644 --- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css +++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css @@ -10,6 +10,7 @@ } .navbar-context-header { + float: left; line-height: 30px; font-size: 15px; } diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.js b/server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx similarity index 81% rename from server/sonar-web/src/main/js/components/nav/ContextNavBar.js rename to server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx index 20690b55881..de836252b20 100644 --- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.js +++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx @@ -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 ; } diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.js b/server/sonar-web/src/main/js/components/nav/NavBar.tsx similarity index 86% rename from server/sonar-web/src/main/js/components/nav/NavBar.js rename to server/sonar-web/src/main/js/components/nav/NavBar.tsx index 180ca0df9e5..08d0e18b511 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBar.js +++ b/server/sonar-web/src/main/js/components/nav/NavBar.tsx @@ -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 (