From: Teryk Bellahsene Date: Mon, 12 Mar 2018 11:06:11 +0000 (+0100) Subject: SONAR-10374 Support pull request in the web app X-Git-Tag: 7.5~1546 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=913c82c8772fd4747626a1fbe665ccda2e5ca9f1;p=sonarqube.git SONAR-10374 Support pull request in the web app --- diff --git a/server/sonar-web/src/main/js/api/branches.ts b/server/sonar-web/src/main/js/api/branches.ts index 02ce5fe693e..9cb5dbaec27 100644 --- a/server/sonar-web/src/main/js/api/branches.ts +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -18,16 +18,28 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { getJSON, post } from '../helpers/request'; +import { Branch, PullRequest } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; -export function getBranches(project: string): Promise { +export function getBranches(project: string): Promise { return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); } -export function deleteBranch(project: string, branch: string): Promise { - return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError); +export function getPullRequests(project: string): Promise { + return getJSON('/api/project_pull_requests/list', { project }).then( + r => r.pullRequests, + throwGlobalError + ); } -export function renameBranch(project: string, name: string): Promise { +export function deleteBranch(data: { branch: string; project: string }) { + return post('/api/project_branches/delete', data).catch(throwGlobalError); +} + +export function deletePullRequest(data: { project: string; pullRequest: string }) { + return post('/api/project_pull_requests/delete', data).catch(throwGlobalError); +} + +export function renameBranch(project: string, name: string) { return post('/api/project_branches/rename', { project, name }).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index db1e32cc643..60f7af8b14b 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -19,7 +19,7 @@ */ import { getJSON, postJSON, post, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -import { Paging, Visibility } from '../app/types'; +import { Paging, Visibility, BranchParameters } from '../app/types'; export interface BaseSearchProjectsParameters { analyzedBefore?: string; @@ -118,11 +118,8 @@ export function getComponentLeaves( } export function getComponent( - componentKey: string, - metrics: string[] = [], - branch?: string + data: { componentKey: string; metricKeys: string } & BranchParameters ): Promise { - const data = { branch, componentKey, metricKeys: metrics.join(',') }; return getJSON('/api/measures/component', data).then(r => r.component); } @@ -130,23 +127,23 @@ export function getTree(component: string, options: RequestData = {}): Promise { - return getJSON('/api/components/show', { component, branch }); +export function getComponentShow(data: { component: string } & BranchParameters): Promise { + return getJSON('/api/components/show', data); } export function getParents(component: string): Promise { - return getComponentShow(component).then(r => r.ancestors); + return getComponentShow({ component }).then(r => r.ancestors); } -export function getBreadcrumbs(component: string, branch?: string): Promise { - return getComponentShow(component, branch).then(r => { +export function getBreadcrumbs(data: { component: string } & BranchParameters): Promise { + return getComponentShow(data).then(r => { const reversedAncestors = [...r.ancestors].reverse(); return [...reversedAncestors, r.component]; }); } -export function getComponentData(component: string, branch?: string): Promise { - return getComponentShow(component, branch).then(r => r.component); +export function getComponentData(data: { component: string } & BranchParameters): Promise { + return getComponentShow(data).then(r => r.component); } export function getMyProjects(data: RequestData): Promise { @@ -246,31 +243,24 @@ export function getSuggestions( return getJSON('/api/components/suggestions', data); } -export function getComponentForSourceViewer(component: string, branch?: string): Promise { - return getJSON('/api/components/app', { component, branch }); +export function getComponentForSourceViewer( + data: { component: string } & BranchParameters +): Promise { + return getJSON('/api/components/app', data); } export function getSources( - component: string, - from?: number, - to?: number, - branch?: string + data: { key: string; from?: number; to?: number } & BranchParameters ): Promise { - const data: RequestData = { key: component, branch }; - if (from) { - Object.assign(data, { from }); - } - if (to) { - Object.assign(data, { to }); - } return getJSON('/api/sources/lines', data).then(r => r.sources); } -export function getDuplications(component: string, branch?: string): Promise { - return getJSON('/api/duplications/show', { key: component, branch }); +export function getDuplications(data: { key: string } & BranchParameters): Promise { + return getJSON('/api/duplications/show', data); } -export function getTests(component: string, line: number | string, branch?: string): Promise { - const data = { sourceFileKey: component, sourceFileLineNumber: line, branch }; +export function getTests( + data: { sourceFileKey: string; sourceFileLineNumber: number | string } & BranchParameters +): Promise { return getJSON('/api/tests/list', data).then(r => r.tests); } diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index bf6f8712090..40d219fcfa3 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -20,17 +20,13 @@ import { getJSON, RequestData, postJSON, post } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; import { Measure, MeasurePeriod } from '../helpers/measures'; -import { Metric, CustomMeasure, Paging } from '../app/types'; +import { Metric, CustomMeasure, Paging, BranchParameters } from '../app/types'; import { Period } from '../helpers/periods'; export function getMeasures( - componentKey: string, - metrics: string[], - branch?: string + data: { componentKey: string; metricKeys: string } & BranchParameters ): Promise<{ metric: string; value?: string }[]> { - const url = '/api/measures/component'; - const data = { componentKey, metricKeys: metrics.join(','), branch }; - return getJSON(url, data).then(r => r.component.measures, throwGlobalError); + return getJSON('/api/measures/component', data).then(r => r.component.measures, throwGlobalError); } interface MeasureComponent { diff --git a/server/sonar-web/src/main/js/api/nav.ts b/server/sonar-web/src/main/js/api/nav.ts index 43a2340cabb..7dcf404ad71 100644 --- a/server/sonar-web/src/main/js/api/nav.ts +++ b/server/sonar-web/src/main/js/api/nav.ts @@ -18,14 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { getJSON, parseJSON, request } from '../helpers/request'; +import { BranchParameters } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; export function getGlobalNavigation(): Promise { return getJSON('/api/navigation/global'); } -export function getComponentNavigation(componentKey: string, branch?: string): Promise { - return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError); +export function getComponentNavigation( + data: { componentKey: string } & BranchParameters +): Promise { + return getJSON('/api/navigation/component', data).catch(throwGlobalError); } export function getSettingsNavigation(): Promise { diff --git a/server/sonar-web/src/main/js/api/projectActivity.ts b/server/sonar-web/src/main/js/api/projectActivity.ts index b931ea5a6f6..fad7562500f 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.ts +++ b/server/sonar-web/src/main/js/api/projectActivity.ts @@ -19,7 +19,7 @@ */ import { getJSON, postJSON, post, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -import { Paging } from '../app/types'; +import { Paging, BranchParameters } from '../app/types'; export interface Event { key: string; @@ -34,13 +34,9 @@ export interface Analysis { events: Event[]; } -export function getProjectActivity(data: { - branch?: string; - project: string; - category?: string; - p?: number; - ps?: number; -}): Promise<{ analyses: Analysis[]; paging: Paging }> { +export function getProjectActivity( + data: { project: string; category?: string; p?: number; ps?: number } & BranchParameters +): Promise<{ analyses: Analysis[]; paging: Paging }> { return getJSON('/api/project_analyses/search', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts index 880d37d65e4..46c0ec67cf8 100644 --- a/server/sonar-web/src/main/js/api/settings.ts +++ b/server/sonar-web/src/main/js/api/settings.ts @@ -20,10 +20,11 @@ import { omitBy } from 'lodash'; import { getJSON, RequestData, post, postJSON } from '../helpers/request'; import { TYPE_PROPERTY_SET } from '../apps/settings/constants'; +import { BranchParameters } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; -export function getDefinitions(component: string | null, branch?: string): Promise { - return getJSON('/api/settings/list_definitions', { branch, component }).then(r => r.definitions); +export function getDefinitions(component?: string): Promise { + return getJSON('/api/settings/list_definitions', { component }).then(r => r.definitions); } export interface SettingValue { @@ -36,21 +37,14 @@ export interface SettingValue { } export function getValues( - keys: string, - component?: string, - branch?: string + data: { keys: string; component?: string } & BranchParameters ): Promise { - return getJSON('/api/settings/values', { keys, component, branch }).then(r => r.settings); + return getJSON('/api/settings/values', data).then(r => r.settings); } -export function setSettingValue( - definition: any, - value: any, - component?: string, - branch?: string -): Promise { +export function setSettingValue(definition: any, value: any, component?: string): Promise { const { key } = definition; - const data: RequestData = { key, component, branch }; + const data: RequestData = { key, component }; if (definition.multiValues) { data.values = value; @@ -65,17 +59,16 @@ export function setSettingValue( return post('/api/settings/set', data); } -export function setSimpleSettingValue(parameters: { - branch?: string; - component?: string; - value: string; - key: string; -}): Promise { - return post('/api/settings/set', parameters).catch(throwGlobalError); +export function setSimpleSettingValue( + data: { component?: string; value: string; key: string } & BranchParameters +): Promise { + return post('/api/settings/set', data).catch(throwGlobalError); } -export function resetSettingValue(key: string, component?: string, branch?: string): Promise { - return post('/api/settings/reset', { keys: key, component, branch }); +export function resetSettingValue( + data: { keys: string; component?: string } & BranchParameters +): Promise { + return post('/api/settings/reset', data); } export function sendTestEmail(to: string, subject: string, message: string): Promise { diff --git a/server/sonar-web/src/main/js/api/tests.ts b/server/sonar-web/src/main/js/api/tests.ts index 1ac56218630..5b649d1d99b 100644 --- a/server/sonar-web/src/main/js/api/tests.ts +++ b/server/sonar-web/src/main/js/api/tests.ts @@ -18,18 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import throwGlobalError from '../app/utils/throwGlobalError'; -import { Paging, TestCase, CoveredFile } from '../app/types'; +import { Paging, TestCase, CoveredFile, BranchParameters } from '../app/types'; import { getJSON } from '../helpers/request'; -export function getTests(parameters: { - branch?: string; - p?: number; - ps?: number; - sourceFileKey?: string; - sourceFileLineNumber?: number; - testFileKey: string; - testId?: string; -}): Promise<{ paging: Paging; tests: TestCase[] }> { +export function getTests( + parameters: { + p?: number; + ps?: number; + sourceFileKey?: string; + sourceFileLineNumber?: number; + testFileKey: string; + testId?: string; + } & BranchParameters +): Promise<{ paging: Paging; tests: TestCase[] }> { return getJSON('/api/tests/list', parameters).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/time-machine.ts b/server/sonar-web/src/main/js/api/time-machine.ts index 2f25912b191..ac77c7c8b41 100644 --- a/server/sonar-web/src/main/js/api/time-machine.ts +++ b/server/sonar-web/src/main/js/api/time-machine.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { getJSON } from '../helpers/request'; -import { Paging } from '../app/types'; +import { Paging, BranchParameters } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; export interface HistoryItem { @@ -39,25 +39,29 @@ interface TimeMachineResponse { } export function getTimeMachineData( - component: string, - metrics: string[], - other?: { branch?: string; p?: number; ps?: number; from?: string; to?: string } + data: { + component: string; + from?: string; + metrics: string; + p?: number; + ps?: number; + to?: string; + } & BranchParameters ): Promise { - return getJSON('/api/measures/search_history', { - component, - metrics: metrics.join(), - ps: 1000, - ...other - }).catch(throwGlobalError); + return getJSON('/api/measures/search_history', data).catch(throwGlobalError); } export function getAllTimeMachineData( - component: string, - metrics: Array, - other?: { branch?: string; p?: number; from?: string; to?: string }, + data: { + component: string; + metrics: string; + from?: string; + p?: number; + to?: string; + } & BranchParameters, prev?: TimeMachineResponse ): Promise { - return getTimeMachineData(component, metrics, { ...other, ps: 1000 }).then(r => { + return getTimeMachineData({ ...data, ps: 1000 }).then(r => { const result = prev ? { measures: prev.measures.map((measure, idx) => ({ @@ -71,11 +75,6 @@ export function getAllTimeMachineData( if (result.paging.pageIndex * result.paging.pageSize >= result.paging.total) { return result; } - return getAllTimeMachineData( - component, - metrics, - { ...other, p: result.paging.pageIndex + 1 }, - result - ); + return getAllTimeMachineData({ ...data, p: result.paging.pageIndex + 1 }, result); }); } diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index af04b9229fb..530bb5f63d4 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -22,25 +22,26 @@ import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import ComponentNav from './nav/component/ComponentNav'; -import { Branch, Component } from '../types'; +import { Component, BranchLike } from '../types'; import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; -import { getBranches } from '../../api/branches'; +import { getBranches, getPullRequests } from '../../api/branches'; import { Task, getTasksForComponent } from '../../api/ce'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/nav'; import { fetchOrganizations } from '../../store/rootActions'; import { STATUSES } from '../../apps/background-tasks/constants'; +import { isPullRequest, isBranch } from '../../helpers/branches'; interface Props { children: any; fetchOrganizations: (organizations: string[]) => void; location: { - query: { branch?: string; id: string }; + query: { branch?: string; id: string; pullRequest?: string }; }; } interface State { - branches: Branch[]; + branchLikes: BranchLike[]; loading: boolean; component?: Component; currentTask?: Task; @@ -57,7 +58,7 @@ export class ComponentContainer extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { branches: [], loading: true }; + this.state = { branchLikes: [], loading: true }; } componentDidMount() { @@ -68,7 +69,8 @@ export class ComponentContainer extends React.PureComponent { componentWillReceiveProps(nextProps: Props) { if ( nextProps.location.query.id !== this.props.location.query.id || - nextProps.location.query.branch !== this.props.location.query.branch + nextProps.location.query.branch !== this.props.location.query.branch || + nextProps.location.query.pullRequest !== this.props.location.query.pullRequest ) { this.fetchComponent(nextProps); } @@ -84,7 +86,7 @@ export class ComponentContainer extends React.PureComponent { }); fetchComponent(props: Props) { - const { branch, id } = props.location.query; + const { branch, id: key, pullRequest } = props.location.query; this.setState({ loading: true }); const onError = (error: any) => { @@ -97,29 +99,33 @@ export class ComponentContainer extends React.PureComponent { } }; - Promise.all([getComponentNavigation(id, branch), getComponentData(id, branch)]).then( - ([nav, data]) => { - const component = this.addQualifier({ ...nav, ...data }); + Promise.all([ + getComponentNavigation({ componentKey: key, branch, pullRequest }), + getComponentData({ component: key, branch, pullRequest }) + ]).then(([nav, data]) => { + const component = this.addQualifier({ ...nav, ...data }); - if (this.context.organizationsEnabled) { - this.props.fetchOrganizations([component.organization]); - } + if (this.context.organizationsEnabled) { + this.props.fetchOrganizations([component.organization]); + } - this.fetchBranches(component).then(branches => { - if (this.mounted) { - this.setState({ loading: false, branches, component }); - } - }, onError); + this.fetchBranches(component).then(branchLikes => { + if (this.mounted) { + this.setState({ loading: false, branchLikes, component }); + } + }, onError); - this.fetchStatus(component); - }, - onError - ); + this.fetchStatus(component); + }, onError); } - fetchBranches = (component: Component) => { + fetchBranches = (component: Component): Promise => { const project = component.breadcrumbs.find(({ qualifier }) => qualifier === 'TRK'); - return project ? getBranches(project.key) : Promise.resolve([]); + return project + ? Promise.all([getBranches(project.key), getPullRequests(project.key)]).then( + ([branches, pullRequests]) => [...branches, ...pullRequests] + ) + : Promise.resolve([]); }; fetchStatus = (component: Component) => { @@ -146,9 +152,9 @@ export class ComponentContainer extends React.PureComponent { handleBranchesChange = () => { if (this.mounted && this.state.component) { this.fetchBranches(this.state.component).then( - branches => { + branchLikes => { if (this.mounted) { - this.setState({ branches }); + this.setState({ branchLikes }); } }, () => {} @@ -158,22 +164,24 @@ export class ComponentContainer extends React.PureComponent { render() { const { query } = this.props.location; - const { branches, component, loading } = this.state; + const { branchLikes, component, loading } = this.state; if (!loading && !component) { return ; } - const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain)); + const branchLike = query.pullRequest + ? branchLikes.find(b => isPullRequest(b) && b.key === query.pullRequest) + : branchLikes.find(b => isBranch(b) && (query.branch ? b.name === query.branch : b.isMain)); return (
{component && !['FIL', 'UTS'].includes(component.qualifier) && ( {
) : ( React.cloneElement(this.props.children, { - branch, - branches, + branchLike, + branchLikes, component, isInProgress: this.state.isInProgress, isPending: this.state.isPending, diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx index 612f4bb043f..e2293316ea9 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx @@ -19,12 +19,12 @@ */ import * as React from 'react'; import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; -import { Component, Branch } from '../types'; +import { BranchLike, Component } from '../types'; interface Props { children: JSX.Element; - branch?: Branch; - branches: Branch[]; + branchLike?: BranchLike; + branchLikes: BranchLike[]; component: Component; isInProgress?: boolean; isPending?: boolean; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index b66a2aff8da..b0917c9d1c7 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -20,18 +20,24 @@ import * as React from 'react'; import { shallow, mount } from 'enzyme'; import { ComponentContainer } from '../ComponentContainer'; -import { getBranches } from '../../../api/branches'; +import { getBranches, getPullRequests } from '../../../api/branches'; import { getTasksForComponent } from '../../../api/ce'; import { getComponentData } from '../../../api/components'; import { getComponentNavigation } from '../../../api/nav'; -jest.mock('../../../api/branches', () => ({ getBranches: jest.fn(() => Promise.resolve([])) })); +jest.mock('../../../api/branches', () => ({ + getBranches: jest.fn(() => Promise.resolve([])), + getPullRequests: jest.fn(() => Promise.resolve([])) +})); + jest.mock('../../../api/ce', () => ({ getTasksForComponent: jest.fn(() => Promise.resolve({ queue: [] })) })); + jest.mock('../../../api/components', () => ({ getComponentData: jest.fn(() => Promise.resolve({})) })); + jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn(() => Promise.resolve({ @@ -49,10 +55,11 @@ jest.mock('../nav/component/ComponentNav', () => ({ const Inner = () =>
; beforeEach(() => { - (getBranches as jest.Mock).mockClear(); - (getComponentData as jest.Mock).mockClear(); - (getComponentNavigation as jest.Mock).mockClear(); - (getTasksForComponent as jest.Mock).mockClear(); + (getBranches as jest.Mock).mockClear(); + (getPullRequests as jest.Mock).mockClear(); + (getComponentData as jest.Mock).mockClear(); + (getComponentNavigation as jest.Mock).mockClear(); + (getTasksForComponent as jest.Mock).mockClear(); }); it('changes component', () => { @@ -90,8 +97,9 @@ it("loads branches for module's project", async () => { await new Promise(setImmediate); expect(getBranches).toBeCalledWith('projectKey'); - expect(getComponentData).toBeCalledWith('moduleKey', undefined); - expect(getComponentNavigation).toBeCalledWith('moduleKey', undefined); + expect(getPullRequests).toBeCalledWith('projectKey'); + expect(getComponentData).toBeCalledWith({ component: 'moduleKey', branch: undefined }); + expect(getComponentNavigation).toBeCalledWith({ componentKey: 'moduleKey', branch: undefined }); }); it("doesn't load branches portfolio", async () => { @@ -103,8 +111,12 @@ it("doesn't load branches portfolio", async () => { await new Promise(setImmediate); expect(getBranches).not.toBeCalled(); - expect(getComponentData).toBeCalledWith('portfolioKey', undefined); - expect(getComponentNavigation).toBeCalledWith('portfolioKey', undefined); + expect(getPullRequests).not.toBeCalled(); + expect(getComponentData).toBeCalledWith({ component: 'portfolioKey', branch: undefined }); + expect(getComponentNavigation).toBeCalledWith({ + componentKey: 'portfolioKey', + branch: undefined + }); wrapper.update(); expect(wrapper.find(Inner).exists()).toBeTruthy(); }); @@ -123,6 +135,7 @@ it('updates branches on change', () => { }); (wrapper.find(Inner).prop('onBranchesChange') as Function)(); expect(getBranches).toBeCalledWith('projectKey'); + expect(getPullRequests).toBeCalledWith('projectKey'); }); it('loads organization', async () => { 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 8b13a6ef075..ac7ce95e3f3 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 @@ -25,6 +25,10 @@ font-size: var(--baseFontSize); } +.navbar-context-meta-branch-menu-title { + padding-left: calc(3 * var(--gridSize)); +} + .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.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 71380876edf..38fbd68f644 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -24,15 +24,15 @@ import ComponentNavMenu from './ComponentNavMenu'; import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; import RecentHistory from '../../RecentHistory'; import * as theme from '../../../theme'; -import { Branch, Component } from '../../../types'; +import { BranchLike, Component } from '../../../types'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; import { Task } from '../../../../api/ce'; import { STATUSES } from '../../../../apps/background-tasks/constants'; import './ComponentNav.css'; interface Props { - branches: Branch[]; - currentBranch?: Branch; + branchLikes: BranchLike[]; + currentBranchLike: BranchLike | undefined; component: Component; currentTask?: Task; isInProgress?: boolean; @@ -85,15 +85,18 @@ export default class ComponentNav extends React.PureComponent { height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - + ) => { event.preventDefault(); event.stopPropagation(); @@ -130,32 +131,46 @@ export default class ComponentNavBranch extends React.PureComponent ) : null; }; renderMergeBranch = () => { - const { currentBranch } = this.props; - if (!isShortLivingBranch(currentBranch)) { + const { currentBranchLike } = this.props; + if (isShortLivingBranch(currentBranchLike)) { + return currentBranchLike.isOrphan ? ( + + {translate('branches.orphan_branch')} + + + + + ) : ( + + {translate('from')} {currentBranchLike.mergeBranch} + + ); + } else if (isPullRequest(currentBranchLike)) { + return ( + + {currentBranchLike.base}, + branch: {currentBranchLike.branch} + }} + /> + + ); + } else { return null; } - return currentBranch.isOrphan ? ( - - {translate('branches.orphan_branch')} - - - - - ) : ( - - {translate('from')} {currentBranch.mergeBranch} - - ); }; renderSingleBranchPopup = () => ( @@ -187,27 +202,33 @@ export default class ComponentNavBranch extends React.PureComponent - - {currentBranch.name} + + {displayName} {this.renderNoBranchSupportPopup()}
); } - if (branches.length < 2) { + if (branchLikes.length < 2) { return (
- - {currentBranch.name} + + {displayName} {this.renderSingleBranchPopup()}
); @@ -219,9 +240,9 @@ export default class ComponentNavBranch extends React.PureComponent
- - - {currentBranch.name} + + + {displayName} 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 index b46da2bf13d..4f2849bdfd8 100644 --- 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 @@ -21,28 +21,32 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; -import { Branch, Component } from '../../../types'; +import { BranchLike, Component } from '../../../types'; import { sortBranchesAsTree, isLongLivingBranch, - isShortLivingBranch + isShortLivingBranch, + isSameBranchLike, + getBranchLikeKey, + isPullRequest, + isBranch } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; -import { getProjectBranchUrl } from '../../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../../helpers/urls'; import SearchBox from '../../../../components/controls/SearchBox'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { - branches: Branch[]; + branchLikes: BranchLike[]; canAdmin?: boolean; component: Component; - currentBranch: Branch; + currentBranchLike: BranchLike; onClose: () => void; } interface State { query: string; - selected: string | null; + selected: BranchLike | undefined; } export default class ComponentNavBranchesMenu extends React.PureComponent { @@ -54,7 +58,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent - sortBranchesAsTree(this.props.branches).filter(branch => - branch.name.toLowerCase().includes(this.state.query.toLowerCase()) - ); + getFilteredBranchLikes = () => { + const query = this.state.query.toLowerCase(); + return sortBranchesAsTree(this.props.branchLikes).filter(branchLike => { + const matchBranchName = isBranch(branchLike) && branchLike.name.toLowerCase().includes(query); + const matchPullRequestTitleOrId = + isPullRequest(branchLike) && + (branchLike.title.includes(query) || branchLike.key.includes(query)); + return matchBranchName || matchPullRequestTitleOrId; + }); + }; handleClickOutside = (event: Event) => { if (!this.node || !this.node.contains(event.target as HTMLElement)) { @@ -76,7 +86,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent this.setState({ query, selected: null }); + handleSearchChange = (query: string) => this.setState({ query, selected: undefined }); handleKeyDown = (event: React.KeyboardEvent) => { switch (event.keyCode) { @@ -99,32 +109,31 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { const selected = this.getSelected(); - const branch = this.getFilteredBranches().find(branch => branch.name === selected); - if (branch) { - this.context.router.push(this.getProjectBranchUrl(branch)); + if (selected) { + this.context.router.push(this.getProjectBranchUrl(selected)); } }; selectPrevious = () => { const selected = this.getSelected(); - const branches = this.getFilteredBranches(); - const index = branches.findIndex(branch => branch.name === selected); + const branchLikes = this.getFilteredBranchLikes(); + const index = branchLikes.findIndex(b => isSameBranchLike(b, selected)); if (index > 0) { - this.setState({ selected: branches[index - 1].name }); + this.setState({ selected: branchLikes[index - 1] }); } }; selectNext = () => { const selected = this.getSelected(); - const branches = this.getFilteredBranches(); - const index = branches.findIndex(branch => branch.name === selected); + const branches = this.getFilteredBranchLikes(); + const index = branches.findIndex(b => isSameBranchLike(b, selected)); if (index >= 0 && index < branches.length - 1) { - this.setState({ selected: branches[index + 1].name }); + this.setState({ selected: branches[index + 1] }); } }; - handleSelect = (branch: Branch) => { - this.setState({ selected: branch.name }); + handleSelect = (branchLike: BranchLike) => { + this.setState({ selected: branchLike }); }; getSelected = () => { @@ -132,21 +141,24 @@ export default class ComponentNavBranchesMenu extends React.PureComponent b.name === this.props.currentBranch.name)) { - return this.props.currentBranch.name; + const branchLikes = this.getFilteredBranchLikes(); + if (branchLikes.find(b => isSameBranchLike(b, this.props.currentBranchLike))) { + return this.props.currentBranchLike; } - if (branches.length > 0) { - return branches[0].name; + if (branchLikes.length > 0) { + return branchLikes[0]; } return undefined; }; - getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.component.key, branch); + getProjectBranchUrl = (branchLike: BranchLike) => + getBranchLikeUrl(this.props.component.key, branchLike); - isSelected = (branch: Branch) => branch.name === this.getSelected(); + isOrphan = (branchLike: BranchLike) => { + return (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && branchLike.isOrphan; + }; renderSearch = () => (
@@ -161,21 +173,26 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { - const branches = this.getFilteredBranches(); + const branchLikes = this.getFilteredBranchLikes(); const selected = this.getSelected(); - if (branches.length === 0) { + if (branchLikes.length === 0) { return
{translate('no_results')}
; } - const items = branches.map((branch, index) => { - const isOrphan = isShortLivingBranch(branch) && branch.isOrphan; - const previous = index > 0 ? branches[index - 1] : undefined; - const isPreviousOrphan = isShortLivingBranch(previous) ? previous.isOrphan : false; - const showDivider = isLongLivingBranch(branch) || (isOrphan && !isPreviousOrphan); + const items = branchLikes.map((branchLike, index) => { + const isOrphan = this.isOrphan(branchLike); + const previous = index > 0 ? branchLikes[index - 1] : undefined; + const isPreviousOrphan = previous !== undefined && this.isOrphan(previous); + const showDivider = isLongLivingBranch(branchLike) || (isOrphan && !isPreviousOrphan); const showOrphanHeader = isOrphan && !isPreviousOrphan; + const showPullRequestHeader = + !showOrphanHeader && isPullRequest(branchLike) && !isPullRequest(previous); + const showShortLivingBranchHeader = + !showOrphanHeader && isShortLivingBranch(branchLike) && !isShortLivingBranch(previous); + return ( - + {showDivider &&
  • } {showOrphanHeader && (
  • @@ -185,12 +202,22 @@ export default class ComponentNavBranchesMenu extends React.PureComponent
  • )} + {showPullRequestHeader && ( +
  • + {translate('branches.pull_requests')} +
  • + )} + {showShortLivingBranchHeader && ( +
  • + {translate('branches.short_lived_branches')} +
  • + )}
    ); 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 index b98de6bba7b..9375bbf356c 100644 --- 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 @@ -21,47 +21,55 @@ import * as React from 'react'; import { Link } from 'react-router'; import * as classNames from 'classnames'; import BranchStatus from '../../../../components/common/BranchStatus'; -import { Branch, Component } from '../../../types'; +import { BranchLike, Component } from '../../../types'; import BranchIcon from '../../../../components/icons-components/BranchIcon'; -import { isShortLivingBranch } from '../../../../helpers/branches'; +import { + isShortLivingBranch, + getBranchLikeDisplayName, + getBranchLikeKey, + isMainBranch, + isPullRequest +} from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; -import { getProjectBranchUrl } from '../../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../../helpers/urls'; import Tooltip from '../../../../components/controls/Tooltip'; export interface Props { - branch: Branch; + branchLike: BranchLike; component: Component; - onSelect: (branch: Branch) => void; + onSelect: (branchLike: BranchLike) => void; selected: boolean; } -export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { +export default function ComponentNavBranchesMenuItem({ branchLike, ...props }: Props) { const handleMouseEnter = () => { - props.onSelect(branch); + props.onSelect(branchLike); }; + const displayName = getBranchLikeDisplayName(branchLike); + const shouldBeIndented = + (isShortLivingBranch(branchLike) && !branchLike.isOrphan) || isPullRequest(branchLike); + return ( -
  • - +
  • + + to={getBranchLikeUrl(props.component.key, branchLike)}>
    - {branch.name} - {branch.isMain && ( + {displayName} + {isMainBranch(branchLike) && (
    {translate('branches.main_branch')}
    )}
    - +
    diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx index ede0242991a..b0d387e4fa7 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import ComponentNavBranch from './ComponentNavBranch'; -import { Component, Organization, Branch, Breadcrumb } from '../../../types'; +import { Component, Organization, BranchLike, Breadcrumb } from '../../../types'; import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer'; import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; @@ -37,9 +37,9 @@ interface StateProps { } interface OwnProps { - branches: Branch[]; + branchLikes: BranchLike[]; component: Component; - currentBranch?: Branch; + currentBranchLike: BranchLike | undefined; location?: any; } @@ -70,11 +70,11 @@ export function ComponentNavHeader(props: Props) { {component.visibility === 'private' && ( )} - {props.currentBranch && ( + {props.currentBranchLike && ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index a52d9a31a51..80f9ebb31de 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -21,12 +21,13 @@ import * as React from 'react'; import { Link } from 'react-router'; import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; -import { Branch, Component, Extension } from '../../../types'; +import { BranchLike, Component, Extension } from '../../../types'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; import { isShortLivingBranch, - getBranchName, - isLongLivingBranch + isPullRequest, + isMainBranch, + getBranchLikeQuery } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; @@ -47,7 +48,7 @@ const SETTINGS_URLS = [ ]; interface Props { - branch?: Branch; + branchLike: BranchLike | undefined; component: Component; location?: any; } @@ -78,23 +79,21 @@ export default class ComponentNavMenu extends React.PureComponent { return this.props.component.configuration || {}; } + getQuery = () => { + return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) }; + }; + renderDashboardLink() { - if (isShortLivingBranch(this.props.branch)) { + const { branchLike } = this.props; + + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { return null; } const pathname = this.isPortfolio() ? '/portfolio' : '/dashboard'; return (
  • - + {translate('overview.page')}
  • @@ -108,15 +107,7 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • - + {this.isPortfolio() || this.isApplication() ? translate('view_projects.page') : translate('code.page')} @@ -126,20 +117,16 @@ export default class ComponentNavMenu extends React.PureComponent { } renderActivityLink() { - if (isShortLivingBranch(this.props.branch)) { + const { branchLike } = this.props; + + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { return null; } return (
  • {translate('project_activity.page')} @@ -153,16 +140,9 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • + to={{ pathname: '/project/issues', query: { ...this.getQuery(), resolved: 'false' } }}> {translate('issues.page')}
  • @@ -170,20 +150,16 @@ export default class ComponentNavMenu extends React.PureComponent { } renderComponentMeasuresLink() { - if (isShortLivingBranch(this.props.branch)) { + const { branchLike } = this.props; + + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { return null; } return (
  • {translate('layout.measures')} @@ -192,30 +168,14 @@ export default class ComponentNavMenu extends React.PureComponent { } renderAdministration() { - const { branch } = this.props; + const { branchLike } = this.props; - if (!this.getConfiguration().showSettings || (branch && !branch.isMain)) { + if (!this.getConfiguration().showSettings || (branchLike && !isMainBranch(branchLike))) { return null; } const isSettingsActive = SETTINGS_URLS.some(url => window.location.href.indexOf(url) !== -1); - if (isLongLivingBranch(branch)) { - return ( -
  • - - {translate('branches.branch_settings')} - -
  • - ); - } - const adminLinks = this.renderAdministrationLinks(); if (!adminLinks.some(link => link != null)) { return null; @@ -260,13 +220,7 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • {translate('project_settings.page')} @@ -448,7 +402,7 @@ export default class ComponentNavMenu extends React.PureComponent { }; renderAdminExtensions() { - if (this.props.branch && !this.props.branch.isMain) { + if (this.props.branchLike && !isMainBranch(this.props.branchLike)) { return []; } const extensions = this.getConfiguration().extensions || []; @@ -457,7 +411,7 @@ export default class ComponentNavMenu extends React.PureComponent { renderExtensions() { const extensions = this.props.component.extensions || []; - if (!extensions.length || (this.props.branch && !this.props.branch.isMain)) { + if (!extensions.length || (this.props.branchLike && !isMainBranch(this.props.branchLike))) { return null; } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index 9bee1a224fe..57f41ffeb3d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -19,7 +19,14 @@ */ import * as React from 'react'; import { connect } from 'react-redux'; -import { Branch, Component, CurrentUser, isLoggedIn, HomePageType, HomePage } from '../../../types'; +import { + BranchLike, + Component, + CurrentUser, + isLoggedIn, + HomePageType, + HomePage +} from '../../../types'; import BranchStatus from '../../../../components/common/BranchStatus'; import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; import Favorite from '../../../../components/controls/Favorite'; @@ -28,7 +35,8 @@ import Tooltip from '../../../../components/controls/Tooltip'; import { isShortLivingBranch, isLongLivingBranch, - getBranchName + isMainBranch, + isPullRequest } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import { getCurrentUser } from '../../../../store/rootReducer'; @@ -38,27 +46,15 @@ interface StateProps { } interface Props extends StateProps { - branch?: Branch; + branchLike?: BranchLike; component: Component; } -export function ComponentNavMeta({ branch, component, currentUser }: Props) { - const shortBranch = isShortLivingBranch(branch); - const mainBranch = !branch || branch.isMain; - const longBranch = isLongLivingBranch(branch); - - let currentPage: HomePage | undefined; - if (component.qualifier === 'VW' || component.qualifier === 'SVW') { - currentPage = { type: HomePageType.Portfolio, component: component.key }; - } else if (component.qualifier === 'APP') { - currentPage = { type: HomePageType.Application, component: component.key }; - } else if (component.qualifier === 'TRK') { - currentPage = { - type: HomePageType.Project, - component: component.key, - branch: getBranchName(branch) - }; - } +export function ComponentNavMeta({ branchLike, component, currentUser }: Props) { + const mainBranch = !branchLike || isMainBranch(branchLike); + const longBranch = isLongLivingBranch(branchLike); + const currentPage = getCurrentPage(component, branchLike); + const displayVersion = component.version !== undefined && (mainBranch || longBranch); return (
    @@ -67,14 +63,13 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) {
    )} - {component.version && - !shortBranch && ( - -
    - {translate('version')} {component.version} -
    -
    - )} + {displayVersion && ( + +
    + {translate('version')} {component.version} +
    +
    + )} {isLoggedIn(currentUser) && (
    {mainBranch && ( @@ -90,15 +85,36 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) { )}
    )} - {shortBranch && ( + {(isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && (
    - + {isPullRequest(branchLike) && + branchLike.url !== undefined && ( + + {translate('branches.see_the_pr')} + + + )} +
    )}
  • ); } +function getCurrentPage(component: Component, branchLike: BranchLike | undefined) { + let currentPage: HomePage | undefined; + if (component.qualifier === 'VW' || component.qualifier === 'SVW') { + currentPage = { type: HomePageType.Portfolio, component: component.key }; + } else if (component.qualifier === 'APP') { + currentPage = { type: HomePageType.Application, component: component.key }; + } else if (component.qualifier === 'TRK') { + const branch = + isMainBranch(branchLike) || isLongLivingBranch(branchLike) ? branchLike.name : undefined; + currentPage = { type: HomePageType.Project, component: component.key, branch }; + } + return currentPage; +} + const mapStateToProps = (state: any): StateProps => ({ currentUser: getCurrentUser(state) }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index e4b72cb3564..b0f633badb5 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -30,7 +30,14 @@ const component = { }; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); wrapper.setState({ isInProgress: true, isPending: true }); expect(wrapper).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 index dee0d5b79ba..7eac067d8dc 100644 --- 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 @@ -25,21 +25,22 @@ import { ShortLivingBranch, MainBranch, Component, - LongLivingBranch + LongLivingBranch, + PullRequest } from '../../../../types'; import { click } from '../../../../../helpers/testUtils'; +const mainBranch: MainBranch = { isMain: true, name: 'master' }; const fooBranch: LongLivingBranch = { isMain: false, name: 'foo', type: BranchType.LONG }; it('renders main branch', () => { - const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; expect( shallow( , { context: { branchesEnabled: true } } ) @@ -58,9 +59,30 @@ it('renders short-living branch', () => { expect( shallow( , + { context: { branchesEnabled: true } } + ) + ).toMatchSnapshot(); +}); + +it('renders pull request', () => { + const pullRequest: PullRequest = { + base: 'master', + branch: 'feature', + key: '1234', + title: 'Feature PR', + url: 'https://example.com/pull/1234' + }; + const component = {} as Component; + expect( + shallow( + , { context: { branchesEnabled: true } } ) @@ -68,13 +90,12 @@ it('renders short-living branch', () => { }); it('opens menu', () => { - const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; const wrapper = shallow( , { context: { branchesEnabled: true } } ); @@ -84,10 +105,13 @@ it('opens menu', () => { }); it('renders single branch popup', () => { - const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; const wrapper = shallow( - , + , { context: { branchesEnabled: true } } ); expect(wrapper).toMatchSnapshot(); @@ -97,13 +121,12 @@ it('renders single branch popup', () => { }); it('renders no branch support popup', () => { - const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; const wrapper = shallow( , { context: { branchesEnabled: false } } ); @@ -114,10 +137,13 @@ it('renders no branch support popup', () => { }); it('renders nothing on SonarCloud without branch support', () => { - const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; const wrapper = shallow( - , + , { context: { branchesEnabled: false, onSonarCloud: true } } ); expect(wrapper.type()).toBeNull(); 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 index ec4843e138f..bb43ec60c63 100644 --- 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 @@ -25,7 +25,8 @@ import { MainBranch, ShortLivingBranch, LongLivingBranch, - Component + Component, + PullRequest } from '../../../../types'; import { elementKeydown } from '../../../../../helpers/testUtils'; @@ -35,9 +36,15 @@ it('renders list', () => { expect( shallow( ) @@ -47,9 +54,9 @@ it('renders list', () => { it('searches', () => { const wrapper = shallow( ); @@ -60,21 +67,21 @@ it('searches', () => { it('selects next & previous', () => { const wrapper = shallow( ); elementKeydown(wrapper.find('SearchBox'), 40); wrapper.update(); - expect(wrapper.state().selected).toBe('foo'); + expect(wrapper.state().selected).toEqual(shortBranch('foo')); elementKeydown(wrapper.find('SearchBox'), 40); wrapper.update(); - expect(wrapper.state().selected).toBe('foobar'); + expect(wrapper.state().selected).toEqual(shortBranch('foobar')); elementKeydown(wrapper.find('SearchBox'), 38); wrapper.update(); - expect(wrapper.state().selected).toBe('foo'); + expect(wrapper.state().selected).toEqual(shortBranch('foo')); }); function mainBranch(): MainBranch { @@ -95,3 +102,13 @@ function shortBranch(name: string, isOrphan?: true): ShortLivingBranch { function longBranch(name: string): LongLivingBranch { return { isMain: false, name, type: BranchType.LONG }; } + +function pullRequest(title: string): PullRequest { + return { + base: 'master', + branch: 'feature', + key: '1234', + status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, + title + }; +} 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 index b411df4578a..0635bea82a8 100644 --- 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 @@ -35,7 +35,7 @@ const shortBranch: ShortLivingBranch = { const mainBranch: MainBranch = { isMain: true, name: 'master' }; it('renders main branch', () => { - expect(shallowRender({ branch: mainBranch })).toMatchSnapshot(); + expect(shallowRender({ branchLike: mainBranch })).toMatchSnapshot(); }); it('renders short-living branch', () => { @@ -43,13 +43,14 @@ it('renders short-living branch', () => { }); it('renders short-living orhpan branch', () => { - expect(shallowRender({ branch: { ...shortBranch, isOrphan: true } })).toMatchSnapshot(); + const orhpan: ShortLivingBranch = { ...shortBranch, isOrphan: true }; + expect(shallowRender({ branchLike: orhpan })).toMatchSnapshot(); }); function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { return shallow( { visibility: 'public' }; const result = shallow( - + ); expect(result).toMatchSnapshot(); }); @@ -53,8 +58,9 @@ it('should render organization', () => { }; const result = shallow( @@ -72,7 +78,12 @@ it('renders private badge', () => { visibility: 'private' }; const result = shallow( - + ); expect(result.find('PrivateBadge')).toHaveLength(1); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index eb400536ed1..ef502e33aad 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -39,7 +39,7 @@ it('should work with extensions', () => { extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }; expect( - shallow(, { + shallow(, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -58,7 +58,7 @@ it('should work with multiple extensions', () => { ] }; expect( - shallow(, { + shallow(, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -77,7 +77,7 @@ it('should work for short-living branches', () => { extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }; expect( - shallow(, { + shallow(, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -89,7 +89,7 @@ it('should work for long-living branches', () => { expect( shallow( { function checkWithQualifier(qualifier: string) { const component = { ...baseComponent, configuration: { showSettings: true }, qualifier }; expect( - shallow(, { + shallow(, { context: { branchesEnabled: true } }) ).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 c964e23552a..62bdbdbb715 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,7 +20,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import { ComponentNavMeta } from '../ComponentNavMeta'; -import { BranchType, ShortLivingBranch, LongLivingBranch } from '../../../../types'; +import { BranchType, ShortLivingBranch, LongLivingBranch, PullRequest } from '../../../../types'; const component = { analysisDate: '2017-01-02T00:00:00.000Z', @@ -42,7 +42,11 @@ it('renders status of short-living branch', () => { }; expect( shallow( - + ) ).toMatchSnapshot(); }); @@ -56,7 +60,31 @@ it('renders meta for long-living branch', () => { }; expect( shallow( - + + ) + ).toMatchSnapshot(); +}); + +it('renders meta for pull request', () => { + const pullRequest: PullRequest = { + base: 'master', + branch: 'feature', + key: '1234', + status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, + title: 'Feature PR', + url: 'https://example.com/pull/1234' + }; + expect( + shallow( + ) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index e52606979cf..2d5fe354989 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -6,7 +6,7 @@ exports[`renders 1`] = ` id="context-navigation" > `; +exports[`renders pull request 1`] = ` +
    + + + + + 1234 – Feature PR + + + + + + + master + , + "branch": + feature + , + } + } + /> + +
    +`; + exports[`renders short-living branch 1`] = `
    +
  • + branches.pull_requests +
  • + +
    +
  • @@ -201,10 +233,15 @@ exports[`searches 1`] = ` className="menu menu-vertically-limited" > +
  • + branches.short_lived_branches +
  • 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 index ceba812e99c..781da6de591 100644 --- 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 @@ -2,7 +2,7 @@ exports[`renders main branch 1`] = `
  • `; +exports[`renders meta for pull request 1`] = ` + +`; + exports[`renders status of short-living branch 1`] = `
    dispatch => { const keys = ['sonar.lf.aboutText']; - return getValues(keys.join()).then(values => { + return getValues({ keys: keys.join() }).then(values => { dispatch(receiveValues(values)); }); }; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap index 2fb0f7c15d1..33c554244f1 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap @@ -23,7 +23,6 @@ exports[`should match snapshot 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts index 89dbf7c9705..4361e0d0849 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts @@ -30,7 +30,7 @@ export const fetchMyOrganizations = () => (dispatch: Dispatch) => { }; export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch) => { - return getValues('sonar.organizations.anyoneCanCreate').then(values => { + return getValues({ keys: 'sonar.organizations.anyoneCanCreate' }).then(values => { dispatch(receiveValues(values, undefined)); }); }; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx index 61fba1730da..a81919215b1 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx @@ -23,9 +23,16 @@ import TaskType from './TaskType'; import { Task } from '../types'; import QualifierIcon from '../../../components/shared/QualifierIcon'; import Organization from '../../../components/shared/Organization'; -import { getProjectUrl } from '../../../helpers/urls'; +import { + getProjectUrl, + getShortLivingBranchUrl, + getLongLivingBranchUrl, + getPullRequestUrl +} from '../../../helpers/urls'; import ShortLivingBranchIcon from '../../../components/icons-components/ShortLivingBranchIcon'; import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon'; +import PullRequestIcon from '../../../components/icons-components/PullRequestIcon'; +import Tooltip from '../../../components/controls/Tooltip'; interface Props { task: Task; @@ -45,8 +52,10 @@ export default function TaskComponent({ task }: Props) { {task.branchType === 'SHORT' && } {task.branchType === 'LONG' && } + {task.pullRequest !== undefined && } {!task.branchType && + !task.pullRequest && task.componentQualifier && ( @@ -56,7 +65,7 @@ export default function TaskComponent({ task }: Props) { {task.organization && } {task.componentName && ( - + {task.componentName} {task.branch && ( @@ -65,6 +74,15 @@ export default function TaskComponent({ task }: Props) { {task.branch} )} + + {task.pullRequest && ( + + + / + {task.pullRequest} + + + )} )} @@ -72,3 +90,15 @@ export default function TaskComponent({ task }: Props) { ); } + +function getTaskComponentUrl(componentKey: string, task: Task) { + if (task.branch && task.branchType === 'SHORT') { + return getShortLivingBranchUrl(componentKey, task.branchType); + } else if (task.branchType && task.branchType === 'LONG') { + return getLongLivingBranchUrl(componentKey, task.branchType); + } else if (task.pullRequest) { + return getPullRequestUrl(componentKey, task.pullRequest); + } else { + return getProjectUrl(componentKey); + } +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap index 93a63cdafd4..f71a484187b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap @@ -20,7 +20,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -67,7 +66,6 @@ exports[`renders 3`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": "feature", "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/types.ts b/server/sonar-web/src/main/js/apps/background-tasks/types.ts index b52783299ce..239c4581d61 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/types.ts +++ b/server/sonar-web/src/main/js/apps/background-tasks/types.ts @@ -29,6 +29,8 @@ export interface Task { hasScannerContext?: boolean; id: string; organization?: string; + pullRequest?: string; + pullRequestTitle?: string; startedAt?: string; status: string; submittedAt: string; diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx index b6941e56388..937a66ee751 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -26,16 +26,16 @@ import Search from './Search'; import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; import { Component as CodeComponent } from '../types'; import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '../utils'; +import { Component, BranchLike } from '../../../app/types'; import ListFooter from '../../../components/controls/ListFooter'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; -import { parseError } from '../../../helpers/request'; -import { getBranchName } from '../../../helpers/branches'; +import { isSameBranchLike } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; -import { Component, Branch } from '../../../app/types'; +import { parseError } from '../../../helpers/request'; import '../code.css'; interface Props { - branch?: Branch; + branchLike?: BranchLike; component: Component; location: { query: { [x: string]: string } }; } @@ -67,7 +67,10 @@ export default class App extends React.PureComponent { } componentDidUpdate(prevProps: Props) { - if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { + if ( + prevProps.component !== this.props.component || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) + ) { this.handleComponentChange(); } else if (prevProps.location !== this.props.location) { this.handleUpdate(); @@ -80,14 +83,14 @@ export default class App extends React.PureComponent { } handleComponentChange() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; // we already know component's breadcrumbs, addComponentBreadcrumbs(component.key, component.breadcrumbs); this.setState({ loading: true }); const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); - retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch)) + retrieveComponentChildren(component.key, isPortfolio, branchLike) .then(() => { addComponent(component); if (this.mounted) { @@ -106,7 +109,7 @@ export default class App extends React.PureComponent { this.setState({ loading: true }); const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch)) + retrieveComponent(componentKey, isPortfolio, this.props.branchLike) .then(r => { if (this.mounted) { if (['FIL', 'UTS'].includes(r.component.qualifier)) { @@ -152,7 +155,7 @@ export default class App extends React.PureComponent { return; } const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch)) + loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.branchLike) .then(r => { if (this.mounted) { this.setState({ @@ -177,7 +180,7 @@ export default class App extends React.PureComponent { }; render() { - const { branch, component, location } = this.props; + const { branchLike, component, location } = this.props; const { loading, error, @@ -187,8 +190,6 @@ export default class App extends React.PureComponent { total, sourceViewer } = this.state; - const branchName = getBranchName(branch); - const shouldShowBreadcrumbs = breadcrumbs.length > 1; const componentsClassName = classNames('boxed-group', 'boxed-group-inner', 'spacer-top', { @@ -202,7 +203,7 @@ export default class App extends React.PureComponent { {error &&
    {error}
    } {
    {shouldShowBreadcrumbs && ( - + )} {sourceViewer === undefined && @@ -218,7 +223,7 @@ export default class App extends React.PureComponent {
    @@ -232,7 +237,7 @@ export default class App extends React.PureComponent { {sourceViewer !== undefined && (
    - +
    )}
    diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx index 783b7126fa5..7ea212cd7ee 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx @@ -20,20 +20,21 @@ import * as React from 'react'; import ComponentName from './ComponentName'; import { Component } from '../types'; +import { BranchLike } from '../../../app/types'; interface Props { - branch?: string; + branchLike?: BranchLike; breadcrumbs: Component[]; rootComponent: Component; } -export default function Breadcrumbs({ branch, breadcrumbs, rootComponent }: Props) { +export default function Breadcrumbs({ branchLike, breadcrumbs, rootComponent }: Props) { return (
      {breadcrumbs.map((component, index) => (
    • { render() { const { - branch, + branchLike, component, rootComponent, selected = false, @@ -89,10 +90,10 @@ export default class Component extends React.PureComponent { switch (component.qualifier) { case 'FIL': case 'UTS': - componentAction = ; + componentAction = ; break; default: - componentAction = ; + componentAction = ; } } @@ -121,7 +122,7 @@ export default class Component extends React.PureComponent { + to={getBranchLikeUrl(component.refKey || component.key, branchLike)}> ); diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 7abb5e51a2d..0bd1290d662 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -20,9 +20,11 @@ import * as React from 'react'; import { Link } from 'react-router'; import Truncated from './Truncated'; +import { Component } from '../types'; import * as theme from '../../../app/theme'; +import { BranchLike } from '../../../app/types'; import QualifierIcon from '../../../components/shared/QualifierIcon'; -import { Component } from '../types'; +import { getBranchLikeQuery } from '../../../helpers/branches'; function getTooltip(component: Component) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; @@ -49,7 +51,7 @@ function mostCommitPrefix(strings: string[]) { } interface Props { - branch?: string; + branchLike?: BranchLike; canBrowse?: boolean; component: Component; previous?: Component; @@ -57,7 +59,7 @@ interface Props { } export default function ComponentName(props: Props) { - const { branch, component, rootComponent, previous, canBrowse = false } = props; + const { branchLike, component, rootComponent, previous, canBrowse = false } = props; const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR'; const prefix = areBothDirs && previous !== undefined @@ -83,7 +85,7 @@ export default function ComponentName(props: Props) { ); } else if (canBrowse) { - const query = { id: rootComponent.key, branch }; + const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) }; if (component.key !== rootComponent.key) { Object.assign(query, { selected: component.key }); } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx index eb61f75da93..867f4bd476f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Workspace from '../../../components/workspace/main'; +import { Component } from '../types'; +import { BranchLike } from '../../../app/types'; import PinIcon from '../../../components/shared/pin-icon'; +import Workspace from '../../../components/workspace/main'; import { translate } from '../../../helpers/l10n'; -import { Component } from '../types'; interface Props { - branch?: string; + branchLike?: BranchLike; component: Component; } -export default function ComponentPin({ branch, component }: Props) { +export default function ComponentPin({ branchLike, component }: Props) { const handleClick = (event: React.SyntheticEvent) => { event.preventDefault(); - Workspace.openComponent({ branch, key: component.key }); + Workspace.openComponent({ branchLike, key: component.key }); }; return ( diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index 25e99051994..98d3b569e71 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -22,24 +22,25 @@ import Component from './Component'; import ComponentsEmpty from './ComponentsEmpty'; import ComponentsHeader from './ComponentsHeader'; import { Component as IComponent } from '../types'; +import { BranchLike } from '../../../app/types'; interface Props { baseComponent?: IComponent; - branch?: string; + branchLike?: BranchLike; components: IComponent[]; rootComponent: IComponent; selected?: IComponent; } export default function Components(props: Props) { - const { baseComponent, branch, components, rootComponent, selected } = props; + const { baseComponent, branchLike, components, rootComponent, selected } = props; return ( {baseComponent && ( ( void; @@ -89,7 +91,7 @@ export default class Search extends React.PureComponent { } handleSelectCurrent() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; const { results, selectedIndex } = this.state; if (results != null && selectedIndex != null) { const selected = results[selectedIndex]; @@ -99,7 +101,7 @@ export default class Search extends React.PureComponent { } else { this.context.router.push({ pathname: '/code', - query: { branch, id: component.key, selected: selected.key } + query: { id: component.key, selected: selected.key, ...getBranchLikeQuery(branchLike) } }); } } @@ -125,13 +127,18 @@ export default class Search extends React.PureComponent { handleSearch = (query: string) => { if (this.mounted) { - const { branch, component, onError } = this.props; + const { branchLike, component, onError } = this.props; this.setState({ loading: true }); const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; - getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers }) + getTree(component.key, { + q: query, + s: 'qualifier,name', + qualifiers, + ...getBranchLikeQuery(branchLike) + }) .then(r => { if (this.mounted) { this.setState({ @@ -184,7 +191,7 @@ export default class Search extends React.PureComponent { {results != null && (
      { - return getChildren(componentKey, metrics, { branch, p: page, ps: PAGE_SIZE }).then(r => { + return getChildren(componentKey, metrics, { + p: page, + ps: PAGE_SIZE, + ...getBranchLikeQuery(branchLike) + }).then(r => { if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) { - return requestChildren(componentKey, metrics, page + 1, branch).then(moreComponents => { + return requestChildren(componentKey, metrics, page + 1, branchLike).then(moreComponents => { return [...r.components, ...moreComponents]; }); } @@ -69,9 +75,9 @@ function requestChildren( function requestAllChildren( componentKey: string, metrics: string[], - branch?: string + branchLike?: BranchLike ): Promise { - return requestChildren(componentKey, metrics, 1, branch); + return requestChildren(componentKey, metrics, 1, branchLike); } interface Children { @@ -84,13 +90,13 @@ interface ExpandRootDirFunc { (children: Children): Promise; } -function expandRootDir(metrics: string[], branch?: string): ExpandRootDirFunc { +function expandRootDir(metrics: string[], branchLike?: BranchLike): ExpandRootDirFunc { return function({ components, total, ...other }) { const rootDir = components.find( (component: Component) => component.qualifier === 'DIR' && component.name === '/' ); if (rootDir) { - return requestAllChildren(rootDir.key, metrics, branch).then(rootDirComponents => { + return requestAllChildren(rootDir.key, metrics, branchLike).then(rootDirComponents => { const nextComponents = without([...rootDirComponents, ...components], rootDir); const nextTotal = total + rootDirComponents.length - /* root dir */ 1; return { components: nextComponents, total: nextTotal, ...other }; @@ -133,7 +139,11 @@ function getMetrics(isPortfolio: boolean) { return isPortfolio ? PORTFOLIO_METRICS : METRICS; } -function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branch?: string) { +function retrieveComponentBase( + componentKey: string, + isPortfolio: boolean, + branchLike?: BranchLike +) { const existing = getComponentFromBucket(componentKey); if (existing) { return Promise.resolve(existing); @@ -141,7 +151,11 @@ function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branc const metrics = getMetrics(isPortfolio); - return getComponent(componentKey, metrics, branch).then(component => { + return getComponent({ + componentKey, + metricKeys: metrics.join(), + ...getBranchLikeQuery(branchLike) + }).then(component => { addComponent(component); return component; }); @@ -150,7 +164,7 @@ function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branc export function retrieveComponentChildren( componentKey: string, isPortfolio: boolean, - branch?: string + branchLike?: BranchLike ): Promise<{ components: Component[]; page: number; total: number }> { const existing = getComponentChildren(componentKey); if (existing) { @@ -163,9 +177,13 @@ export function retrieveComponentChildren( const metrics = getMetrics(isPortfolio); - return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' }) + return getChildren(componentKey, metrics, { + ps: PAGE_SIZE, + s: 'qualifier,name', + ...getBranchLikeQuery(branchLike) + }) .then(prepareChildren) - .then(expandRootDir(metrics, branch)) + .then(expandRootDir(metrics, branchLike)) .then(r => { addComponentChildren(componentKey, r.components, r.total, r.page); storeChildrenBase(r.components); @@ -175,18 +193,18 @@ export function retrieveComponentChildren( } function retrieveComponentBreadcrumbs( - componentKey: string, - branch?: string + component: string, + branchLike?: BranchLike ): Promise { - const existing = getComponentBreadcrumbs(componentKey); + const existing = getComponentBreadcrumbs(component); if (existing) { return Promise.resolve(existing); } - return getBreadcrumbs(componentKey, branch) + return getBreadcrumbs({ component, ...getBranchLikeQuery(branchLike) }) .then(skipRootDir) .then(breadcrumbs => { - addComponentBreadcrumbs(componentKey, breadcrumbs); + addComponentBreadcrumbs(component, breadcrumbs); return breadcrumbs; }); } @@ -194,7 +212,7 @@ function retrieveComponentBreadcrumbs( export function retrieveComponent( componentKey: string, isPortfolio: boolean, - branch?: string + branchLike?: BranchLike ): Promise<{ breadcrumbs: Component[]; component: Component; @@ -203,9 +221,9 @@ export function retrieveComponent( total: number; }> { return Promise.all([ - retrieveComponentBase(componentKey, isPortfolio, branch), - retrieveComponentChildren(componentKey, isPortfolio, branch), - retrieveComponentBreadcrumbs(componentKey, branch) + retrieveComponentBase(componentKey, isPortfolio, branchLike), + retrieveComponentChildren(componentKey, isPortfolio, branchLike), + retrieveComponentBreadcrumbs(componentKey, branchLike) ]).then(r => { return { component: r[0], @@ -221,13 +239,17 @@ export function loadMoreChildren( componentKey: string, page: number, isPortfolio: boolean, - branch?: string + branchLike?: BranchLike ): Promise { const metrics = getMetrics(isPortfolio); - return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page }) + return getChildren(componentKey, metrics, { + ps: PAGE_SIZE, + p: page, + ...getBranchLikeQuery(branchLike) + }) .then(prepareChildren) - .then(expandRootDir(metrics, branch)) + .then(expandRootDir(metrics, branchLike)) .then(r => { addComponentChildren(componentKey, r.components, r.total, r.page); storeChildrenBase(r.components); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index 4198c6689fd..d357ff4c15a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -26,7 +26,7 @@ import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import { hasBubbleChart, parseQuery, serializeQuery } from '../utils'; -import { getBranchName } from '../../../helpers/branches'; +import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; import { getDisplayMetrics } from '../../../helpers/measures'; /*:: import type { Component, Query, Period } from '../types'; */ @@ -36,14 +36,14 @@ import { getDisplayMetrics } from '../../../helpers/measures'; import '../style.css'; /*:: type Props = {| - branch?: {}, + branchLike?: { id?: string; name: string }, component: Component, currentUser: { isLoggedIn: boolean }, location: { pathname: string, query: RawQuery }, fetchMeasures: ( component: string, metricsKey: Array, - branch?: string + branchLike?: { id?: string; name: string } ) => Promise<{ component: Component, measures: Array, leakPeriod: ?Period }>, fetchMetrics: () => void, metrics: { [string]: Metric }, @@ -88,7 +88,7 @@ export default class App extends React.PureComponent { componentWillReceiveProps(nextProps /*: Props */) { if ( - nextProps.branch !== this.props.branch || + !isSameBranchLike(nextProps.branchLike, this.props.branchLike) || nextProps.component.key !== this.props.component.key || nextProps.metrics !== this.props.metrics ) { @@ -107,10 +107,10 @@ export default class App extends React.PureComponent { } } - fetchMeasures = ({ branch, component, fetchMeasures, metrics } /*: Props */) => { + fetchMeasures = ({ branchLike, component, fetchMeasures, metrics } /*: Props */) => { this.setState({ loading: true }); const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key); - fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then( + fetchMeasures(component.key, filteredKeys, branchLike).then( ({ measures, leakPeriod }) => { if (this.mounted) { this.setState({ @@ -137,7 +137,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...query, - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component.key } }); @@ -148,7 +148,7 @@ export default class App extends React.PureComponent { if (isLoading) { return ; } - const { branch, component, fetchMeasures, metrics } = this.props; + const { branchLike, component, fetchMeasures, metrics } = this.props; const { leakPeriod } = this.state; const query = parseQuery(this.props.location.query); const metric = metrics[query.metric]; @@ -174,7 +174,7 @@ export default class App extends React.PureComponent { {metric != null && ( */ { const fetchMeasures = ( component /*: string */, metricsKey /*: Array */, - branch /*: string | void */ + branchLike /*: { id?: string; name: string } | void */ ) => (dispatch, getState) => { if (metricsKey.length <= 0) { return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); @@ -58,7 +59,7 @@ const fetchMeasures = ( return getMeasuresAndMeta(component, metricsKey, { additionalFields: 'periods', - branch + ...getBranchLikeQuery(branchLike) }).then(r => { const measures = banQualityGate(r.component).map(measure => enhanceMeasure(measure, getMetrics(getState())) diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js index fedd4d52dfe..7709a31d257 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -22,11 +22,12 @@ import React from 'react'; import key from 'keymaster'; import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; +import { getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component } from '../types'; */ /*:: type Props = {| backToFirst: boolean, - branch?: string, + branchLike?: { id?: string, name: string }, className?: string, component: Component, handleSelect: string => void, @@ -76,7 +77,7 @@ export default class Breadcrumbs extends React.PureComponent { key.unbind('left', 'measures-files'); } - fetchBreadcrumbs = ({ branch, component, rootComponent } /*: Props */) => { + fetchBreadcrumbs = ({ branchLike, component, rootComponent } /*: Props */) => { const isRoot = component.key === rootComponent.key; if (isRoot) { if (this.mounted) { @@ -84,11 +85,13 @@ export default class Breadcrumbs extends React.PureComponent { } return; } - getBreadcrumbs(component.key, branch).then(breadcrumbs => { - if (this.mounted) { - this.setState({ breadcrumbs }); + getBreadcrumbs({ component: component.key, ...getBranchLikeQuery(branchLike) }).then( + breadcrumbs => { + if (this.mounted) { + this.setState({ breadcrumbs }); + } } - }); + ); }; render() { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index a9d9f81c35d..8feb83e1167 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -34,6 +34,7 @@ import { complementary } from '../config/complementary'; import { enhanceComponent, isFileType, isViewType } from '../utils'; import { getProjectUrl } from '../../../helpers/urls'; import { isDiffMetric } from '../../../helpers/measures'; +import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */ /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */ @@ -42,7 +43,7 @@ import { isDiffMetric } from '../../../helpers/measures'; // https://github.com/facebook/flow/issues/3147 // router: { push: ({ pathname: string, query?: RawQuery }) => void } /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, className?: string, component: Component, currentUser: { isLoggedIn: boolean }, @@ -87,7 +88,7 @@ export default class MeasureContent extends React.PureComponent { componentWillReceiveProps(nextProps /*: Props */) { if ( - nextProps.branch !== this.props.branch || + !isSameBranchLike(nextProps.branchLike, this.props.branchLike) || nextProps.component !== this.props.component || nextProps.metric !== this.props.metric ) { @@ -115,7 +116,7 @@ export default class MeasureContent extends React.PureComponent { const strategy = view === 'list' ? 'leaves' : 'children'; const metricKeys = [metric.key]; const opts /*: Object */ = { - branch: this.props.branch, + ...getBranchLikeQuery(this.props.branchLike), metricSortFilter: 'withMeasuresOnly' }; const isDiff = isDiffMetric(metric.key); @@ -225,7 +226,7 @@ export default class MeasureContent extends React.PureComponent { return (
      , - branch?: string + branchLike?: { id?: string; name: string } ) => Promise<{ component: Component, measures: Array }>, leakPeriod?: Period, metric: Metric, @@ -89,7 +89,7 @@ export default class MeasureContentContainer extends React.PureComponent { this.mounted = false; } - fetchMeasure = ({ branch, rootComponent, fetchMeasures, metric, selected } /*: Props */) => { + fetchMeasure = ({ branchLike, rootComponent, fetchMeasures, metric, selected } /*: Props */) => { this.updateLoading({ measure: true }); const metricKeys = [metric.key]; @@ -101,7 +101,7 @@ export default class MeasureContentContainer extends React.PureComponent { metricKeys.push('file_complexity_distribution'); } - fetchMeasures(selected || rootComponent.key, metricKeys, branch).then( + fetchMeasures(selected || rootComponent.key, metricKeys, branchLike).then( ({ component, measures }) => { if (this.mounted) { const measure = measures.find(measure => measure.metric.key === metric.key); @@ -134,7 +134,7 @@ export default class MeasureContentContainer extends React.PureComponent { return ( { + getComponentForSourceViewer({ component }).then(component => { this.setState({ component }); onReceiveComponent(component); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js index 87c543aaa76..5c809374492 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -34,7 +34,7 @@ import { isDiffMetric } from '../../../helpers/measures'; /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, component: Component, components: Array, leakPeriod?: Period, @@ -43,7 +43,7 @@ import { isDiffMetric } from '../../../helpers/measures'; |}; */ export default function MeasureHeader(props /*: Props*/) { - const { branch, component, leakPeriod, measure, secondaryMeasure } = props; + const { branchLike, component, leakPeriod, measure, secondaryMeasure } = props; const { metric } = measure; const isDiff = isDiffMetric(metric.key); return ( @@ -72,7 +72,7 @@ export default function MeasureHeader(props /*: Props*/) { overlay={translate('component_measures.show_metric_history')}> + to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js index a5e79b7a134..0128f7b8b1f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js @@ -27,11 +27,12 @@ import BubbleChart from '../drilldown/BubbleChart'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; import { getComponentLeaves } from '../../../api/components'; import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils'; +import { getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, className?: string, component: Component, currentUser: { isLoggedIn: boolean }, @@ -79,9 +80,10 @@ export default class MeasureOverview extends React.PureComponent { } fetchComponents = (props /*: Props */) => { - const { branch, component, domain, metrics } = props; + const { branchLike, component, domain, metrics } = props; if (isFileType(component)) { - return this.setState({ components: [], paging: null }); + this.setState({ components: [], paging: null }); + return; } const { x, y, size, colors } = getBubbleMetrics(domain, metrics); const metricsKey = [x.key, y.key, size.key]; @@ -89,7 +91,7 @@ export default class MeasureOverview extends React.PureComponent { metricsKey.push(colors.map(metric => metric.key)); } const options = { - branch, + ...getBranchLikeQuery(branchLike), s: 'metric', metricSort: size.key, asc: false, @@ -114,11 +116,11 @@ export default class MeasureOverview extends React.PureComponent { }; renderContent() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; if (isFileType(component)) { return (
      - +
      ); } @@ -135,7 +137,7 @@ export default class MeasureOverview extends React.PureComponent { } render() { - const { branch, component, currentUser, leakPeriod, rootComponent } = this.props; + const { branchLike, component, currentUser, leakPeriod, rootComponent } = this.props; const isLoggedIn = currentUser && currentUser.isLoggedIn; const isFile = isFileType(component); return ( @@ -145,7 +147,7 @@ export default class MeasureOverview extends React.PureComponent {
      { + fetchComponent = ({ branchLike, rootComponent, selected } /*: Props */) => { if (!selected || rootComponent.key === selected) { this.setState({ component: rootComponent }); this.updateLoading({ component: false }); return; } this.updateLoading({ component: true }); - getComponentShow(selected, branch).then( + getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then( ({ component }) => { if (this.mounted) { this.setState({ component }); @@ -122,7 +123,7 @@ export default class MeasureOverviewContainer extends React.PureComponent { return ( { }); it('should render with branch', () => { - expect(shallow().find('Link')).toMatchSnapshot(); + const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' }; + expect( + shallow().find('Link') + ).toMatchSnapshot(); }); it('should display secondary measure too', () => { diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap index d6c9bceb55a..8da497107c3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -86,7 +86,6 @@ exports[`should render correctly 1`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "custom_metrics": "reliability_rating", "graph": "custom", "id": "foo", diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js index 5270c221842..1c0747851f1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js @@ -25,7 +25,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, component: ComponentEnhanced, components: Array, leakPeriod?: Period, @@ -81,7 +81,7 @@ export default class CodeView extends React.PureComponent { }; render() { - const { branch, component } = this.props; - return ; + const { branchLike, component } = this.props; + return ; } } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js index 033a8bcf66b..248b6469e2b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js @@ -23,11 +23,11 @@ import { Link } from 'react-router'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { splitPath } from '../../../helpers/path'; -import { getPathUrlAsString, getProjectUrl } from '../../../helpers/urls'; +import { getPathUrlAsString, getBranchLikeUrl } from '../../../helpers/urls'; /*:: import type { ComponentEnhanced } from '../types'; */ /*:: type Props = { - branch?: string, + branchLike?: { id?: string; name: string }, component: ComponentEnhanced, onClick: string => void }; */ @@ -65,15 +65,16 @@ export default class ComponentCell extends React.PureComponent { } render() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; return (
      - + diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js index 9a0ffde8c05..2fd8d7feb27 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js @@ -28,7 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, components: Array, fetchMore: () => void, handleSelect: string => void, @@ -123,7 +123,7 @@ export default class ListView extends React.PureComponent { return (
      (this.listContainer = elem)}> , handleSelect: string => void, metric: Metric @@ -64,7 +64,7 @@ export default class TreeMapView extends React.PureComponent { } } - getTreemapComponents = ({ branch, components, metric } /*: Props */) => { + getTreemapComponents = ({ branchLike, components, metric } /*: Props */) => { const colorScale = this.getColorScale(metric); return components .map(component => { @@ -95,7 +95,7 @@ export default class TreeMapView extends React.PureComponent { sizeValue ), label: component.name, - link: getProjectUrl(component.refKey || component.key, branch) + link: getBranchLikeUrl(component.refKey || component.key, branchLike) }; }) .filter(Boolean); diff --git a/server/sonar-web/src/main/js/apps/component/components/App.tsx b/server/sonar-web/src/main/js/apps/component/components/App.tsx index 96ff380b906..71fc859ea2f 100644 --- a/server/sonar-web/src/main/js/apps/component/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component/components/App.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { PullRequest, BranchType, ShortLivingBranch } from '../../../app/types'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; interface Props { @@ -26,6 +27,7 @@ interface Props { branch?: string; id: string; line?: string; + pullRequest?: string; }; }; } @@ -45,15 +47,30 @@ export default class App extends React.PureComponent { }; render() { - const { branch, id, line } = this.props.location.query; + const { branch, id, line, pullRequest } = this.props.location.query; const finalLine = line ? Number(line) : undefined; + // TODO find a way to avoid creating this fakeBranchLike + // probably the best way would be to drop this page completely + // and redirect to the Code page + let fakeBranchLike: ShortLivingBranch | PullRequest | undefined = undefined; + if (branch) { + fakeBranchLike = { + isMain: false, + mergeBranch: '', + name: branch, + type: BranchType.SHORT + } as ShortLivingBranch; + } else if (pullRequest) { + fakeBranchLike = { base: '', branch: '', key: pullRequest, title: '' } as PullRequest; + } + return (
      Promise; 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 bc6cf9826ff..85bf0fa458e 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 @@ -59,7 +59,12 @@ import ListFooter from '../../../components/controls/ListFooter'; import EmptySearch from '../../../components/common/EmptySearch'; import FiltersHeader from '../../../components/common/FiltersHeader'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import { getBranchName, isShortLivingBranch } from '../../../helpers/branches'; +import { + isShortLivingBranch, + isSameBranchLike, + getBranchLikeQuery, + isPullRequest +} from '../../../helpers/branches'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import Checkbox from '../../../components/controls/Checkbox'; @@ -69,7 +74,7 @@ import '../styles.css'; /*:: export type Props = { - branch?: { name: string }, + branchLike?: { id?: string; name: string }, component?: Component, currentUser: CurrentUser, fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<*>, @@ -193,7 +198,7 @@ export default class App extends React.PureComponent { const { query: prevQuery } = prevProps.location; if ( prevProps.component !== this.props.component || - prevProps.branch !== this.props.branch || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || !areQueriesEqual(prevQuery, query) || areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) ) { @@ -337,7 +342,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined, open: issue @@ -356,7 +361,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined, open: undefined @@ -399,7 +404,7 @@ export default class App extends React.PureComponent { : undefined; const parameters = { - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), componentKeys: component && component.key, s: 'FILE_LINE', ...serializeQuery(query), @@ -594,7 +599,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, ...changes }), - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined } @@ -610,7 +615,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component && this.props.component.key, myIssues: myIssues ? 'true' : undefined } @@ -637,7 +642,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...DEFAULT_QUERY, - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined } @@ -724,7 +729,7 @@ export default class App extends React.PureComponent { handleReload = () => { this.fetchFirstIssues(); - if (isShortLivingBranch(this.props.branch)) { + if (isShortLivingBranch(this.props.branchLike) || isPullRequest(this.props.branchLike)) { this.props.onBranchesChange(); } }; @@ -892,7 +897,7 @@ export default class App extends React.PureComponent { } renderList() { - const { branch, component, currentUser, organization } = this.props; + const { branchLike, component, currentUser, organization } = this.props; const { issues, openIssue, paging } = this.state; const selectedIndex = this.getSelectedIndex(); const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null; @@ -905,7 +910,7 @@ export default class App extends React.PureComponent {
      {paging.total > 0 && ( {openIssue ? ( - + {limitComponentName(issue.projectName)} @@ -64,14 +69,16 @@ export default function ComponentBreadcrumbs({ branch, component, issue, organiz issue.subProject !== undefined && issue.subProjectName !== undefined && ( - + {limitComponentName(issue.subProjectName)} )} - + {collapsePath(issue.componentLongName)}
      diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js index 30506292162..c7d6bc2e650 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js @@ -25,7 +25,7 @@ import ListItem from './ListItem'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, checked: Array, component?: Component, issues: Array, @@ -44,13 +44,13 @@ export default class IssuesList extends React.PureComponent { /*:: props: Props; */ render() { - const { branch, checked, component, issues, openPopup, selectedIssue } = this.props; + const { branchLike, checked, component, issues, openPopup, selectedIssue } = this.props; return (
      {issues.map((issue, index) => ( Promise<*>, onIssueChange: Issue => void, @@ -107,7 +107,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
      (this.node = node)}> )} { it('renders with branch', () => { const issue = { ...baseIssue, subProject: 'sub-proj', subProjectName: 'sub-proj-name' }; - expect(shallow()).toMatchSnapshot(); + const shortBranch: ShortLivingBranch = { + isMain: false, + mergeBranch: '', + name: 'feature', + type: BranchType.SHORT + }; + expect( + shallow() + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap index 3861f4a0780..ac510c72824 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap @@ -19,7 +19,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "proj", }, } @@ -37,10 +36,10 @@ exports[`renders 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/code", "query": Object { - "branch": undefined, - "id": "comp", + "id": "proj", + "selected": "comp", }, } } @@ -71,10 +70,11 @@ exports[`renders with branch 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { "branch": "feature", "id": "proj", + "resolved": "false", }, } } @@ -94,10 +94,11 @@ exports[`renders with branch 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { "branch": "feature", "id": "sub-proj", + "resolved": "false", }, } } @@ -114,10 +115,11 @@ exports[`renders with branch 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/code", "query": Object { "branch": "feature", - "id": "comp", + "id": "proj", + "selected": "comp", }, } } @@ -150,7 +152,6 @@ exports[`renders with sub-project 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "proj", }, } @@ -173,7 +174,6 @@ exports[`renders with sub-project 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "sub-proj", }, } @@ -191,10 +191,10 @@ exports[`renders with sub-project 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/code", "query": Object { - "branch": undefined, - "id": "comp", + "id": "proj", + "selected": "comp", }, } } diff --git a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx index 3592c4daea3..ebea71b75a6 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx @@ -21,15 +21,16 @@ import * as React from 'react'; import BadgeButton from './BadgeButton'; import BadgeParams from './BadgeParams'; import { BadgeType, BadgeOptions, getBadgeUrl } from './utils'; -import { Metric } from '../../../app/types'; +import { Metric, BranchLike } from '../../../app/types'; import CodeSnippet from '../../../components/common/CodeSnippet'; import Modal from '../../../components/controls/Modal'; +import { getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; import './styles.css'; import { Button, ResetButtonLink } from '../../../components/ui/buttons'; interface Props { - branch?: string; + branchLike?: BranchLike; metrics: { [key: string]: Metric }; project: string; } @@ -64,10 +65,10 @@ export default class BadgesModal extends React.PureComponent { }; render() { - const { branch, project } = this.props; + const { branchLike, project } = this.props; const { selectedType, badgeOptions } = this.state; const header = translate('overview.badges.title'); - const fullBadgeOptions = { branch, project, ...badgeOptions }; + const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) }; return (
      - {sortBranchesAsTree(branches).map(branch => ( + {sortBranchesAsTree(branchLikes).map(branchLike => ( ))} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx index 723998c7049..0d5e440a372 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx @@ -22,10 +22,16 @@ import * as classNames from 'classnames'; import DeleteBranchModal from './DeleteBranchModal'; import LeakPeriodForm from './LeakPeriodForm'; import RenameBranchModal from './RenameBranchModal'; -import { Branch } from '../../../app/types'; +import { BranchLike } from '../../../app/types'; import BranchStatus from '../../../components/common/BranchStatus'; import BranchIcon from '../../../components/icons-components/BranchIcon'; -import { isShortLivingBranch, isLongLivingBranch } from '../../../helpers/branches'; +import { + isShortLivingBranch, + isLongLivingBranch, + isMainBranch, + getBranchLikeDisplayName, + isPullRequest +} from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; import DateFromNow from '../../../components/intl/DateFromNow'; import ActionsDropdown, { @@ -34,8 +40,9 @@ import ActionsDropdown, { } from '../../../components/controls/ActionsDropdown'; interface Props { - branch: Branch; + branchLike: BranchLike; component: string; + isOrphan?: boolean; onChange: () => void; } @@ -91,19 +98,19 @@ export default class BranchRow extends React.PureComponent { }; renderActions() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; return ( ); } render() { - const { branch } = this.props; + const { branchLike, isOrphan } = this.props; + const indented = (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && !isOrphan; return ( {this.renderActions()} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx index 127b3781189..c98bb190f65 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx @@ -18,14 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { deleteBranch } from '../../../api/branches'; -import { Branch } from '../../../app/types'; +import { deleteBranch, deletePullRequest } from '../../../api/branches'; +import { BranchLike } from '../../../app/types'; import Modal from '../../../components/controls/Modal'; import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { isPullRequest, getBranchLikeDisplayName } from '../../../helpers/branches'; interface Props { - branch: Branch; + branchLike: BranchLike; component: string; onClose: () => void; onDelete: () => void; @@ -50,7 +51,16 @@ export default class DeleteBranchModal extends React.PureComponent handleSubmit = (event: React.SyntheticEvent) => { event.preventDefault(); this.setState({ loading: true }); - deleteBranch(this.props.component, this.props.branch.name).then( + const request = isPullRequest(this.props.branchLike) + ? deletePullRequest({ + project: this.props.component, + pullRequest: this.props.branchLike.key + }) + : deleteBranch({ + branch: this.props.branchLike.name, + project: this.props.component + }); + request.then( () => { if (this.mounted) { this.setState({ loading: false }); @@ -66,8 +76,10 @@ export default class DeleteBranchModal extends React.PureComponent }; render() { - const { branch } = this.props; - const header = translate('branches.delete'); + const { branchLike } = this.props; + const header = translate( + isPullRequest(branchLike) ? 'branches.pull_request.delete' : 'branches.delete' + ); return ( @@ -76,7 +88,12 @@ export default class DeleteBranchModal extends React.PureComponent
      - {translateWithParameters('branches.delete.are_you_sure', branch.name)} + {translateWithParameters( + isPullRequest(branchLike) + ? 'branches.pull_request.delete.are_you_sure' + : 'branches.delete.are_you_sure', + getBranchLikeDisplayName(branchLike) + )}
      {this.state.loading && } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx index 71d4deaa53f..3bb22c48aed 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx @@ -53,7 +53,7 @@ export default class LeakPeriodForm extends React.PureComponent { fetchSetting() { this.setState({ loading: true }); - getValues(LEAK_PERIOD, this.props.project, this.props.branch).then( + getValues({ keys: LEAK_PERIOD, component: this.props.project, branch: this.props.branch }).then( settings => { if (this.mounted) { this.setState({ loading: false, setting: settings[0] }); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx index e6d8b31d0f2..61c9e133c4c 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx @@ -48,7 +48,7 @@ export default class LongBranchesPattern extends React.PureComponent { if (this.mounted) { this.setState({ setting: settings[0] }); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx index bc78f0395cb..2e8ee656d44 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import { renameBranch } from '../../../api/branches'; -import { Branch } from '../../../app/types'; +import { MainBranch } from '../../../app/types'; import Modal from '../../../components/controls/Modal'; import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; interface Props { - branch: Branch; + branch: MainBranch; component: string; onClose: () => void; onRename: () => void; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx index 6c3b737dd4c..45533ab1da6 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx @@ -51,6 +51,12 @@ export default class SettingForm extends React.PureComponent { this.mounted = false; } + stopLoading = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + handleSubmit = (event: React.SyntheticEvent) => { event.preventDefault(); @@ -65,11 +71,7 @@ export default class SettingForm extends React.PureComponent { component: this.props.project, key: this.props.setting.key, value - }).then(this.props.onChange, () => { - if (this.mounted) { - this.setState({ submitting: false }); - } - }); + }).then(this.props.onChange, this.stopLoading); }; handleValueChange = (event: React.SyntheticEvent) => { @@ -78,14 +80,11 @@ export default class SettingForm extends React.PureComponent { handleResetClick = () => { this.setState({ submitting: true }); - resetSettingValue(this.props.setting.key, this.props.project, this.props.branch).then( - this.props.onChange, - () => { - if (this.mounted) { - this.setState({ submitting: false }); - } - } - ); + resetSettingValue({ + keys: this.props.setting.key, + component: this.props.project, + branch: this.props.branch + }).then(this.props.onChange, this.stopLoading); }; render() { diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx index e22b327bbc1..ee90b112e98 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx @@ -25,7 +25,13 @@ jest.mock('../../../../api/settings', () => ({ import * as React from 'react'; import { mount, shallow } from 'enzyme'; import App from '../App'; -import { BranchType, MainBranch, LongLivingBranch, ShortLivingBranch } from '../../../../app/types'; +import { + BranchType, + LongLivingBranch, + ShortLivingBranch, + MainBranch, + PullRequest +} from '../../../../app/types'; const getValues = require('../../../../api/settings').getValues as jest.Mock; @@ -34,19 +40,27 @@ beforeEach(() => { }); it('renders sorted list of branches', () => { - const branches: [MainBranch, LongLivingBranch, ShortLivingBranch] = [ + const branchLikes: [MainBranch, LongLivingBranch, ShortLivingBranch, PullRequest] = [ { isMain: true, name: 'master' }, { isMain: false, name: 'branch-1.0', type: BranchType.LONG }, - { isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT } + { isMain: false, mergeBranch: 'master', name: 'feature', type: BranchType.SHORT }, + { base: 'master', branch: 'feature', key: '1234', title: 'Feature PR' } ]; const wrapper = shallow( - + ); wrapper.setState({ branchLifeTime: '100', loading: false }); expect(wrapper).toMatchSnapshot(); }); it('fetches branch life time setting on mount', () => { - mount(); - expect(getValues).toBeCalledWith('sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches'); + mount(); + expect(getValues).toBeCalledWith({ + keys: 'sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches' + }); }); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx index 8d7532033cc..eac799c7f6f 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx @@ -20,7 +20,13 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import BranchRow from '../BranchRow'; -import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types'; +import { + MainBranch, + ShortLivingBranch, + BranchType, + PullRequest, + BranchLike +} from '../../../../app/types'; import { click } from '../../../../helpers/testUtils'; const mainBranch: MainBranch = { isMain: true, name: 'master' }; @@ -33,6 +39,13 @@ const shortBranch: ShortLivingBranch = { type: BranchType.SHORT }; +const pullRequest: PullRequest = { + base: 'master', + branch: 'feature', + key: '1234', + title: 'Feature PR' +}; + it('renders main branch', () => { expect(shallowRender(mainBranch)).toMatchSnapshot(); }); @@ -41,6 +54,10 @@ it('renders short-living branch', () => { expect(shallowRender(shortBranch)).toMatchSnapshot(); }); +it('renders pull request', () => { + expect(shallowRender(pullRequest)).toMatchSnapshot(); +}); + it('renames main branch', () => { const onChange = jest.fn(); const wrapper = shallowRender(mainBranch, onChange); @@ -59,8 +76,19 @@ it('deletes short-living branch', () => { expect(onChange).toBeCalled(); }); -function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) { - const wrapper = shallow(); +it('deletes pull request', () => { + const onChange = jest.fn(); + const wrapper = shallowRender(pullRequest, onChange); + + click(wrapper.find('.js-delete')); + (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)(); + expect(onChange).toBeCalled(); +}); + +function shallowRender(branchLike: BranchLike, onChange: () => void = jest.fn()) { + const wrapper = shallow( + + ); (wrapper.instance() as any).mounted = true; return wrapper; } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx index 88fd8dffca7..16d7dfaf5cf 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx @@ -18,42 +18,72 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* eslint-disable import/first */ -jest.mock('../../../../api/branches', () => ({ deleteBranch: jest.fn() })); +jest.mock('../../../../api/branches', () => ({ + deleteBranch: jest.fn(), + deletePullRequest: jest.fn() +})); import * as React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import DeleteBranchModal from '../DeleteBranchModal'; -import { ShortLivingBranch, BranchType } from '../../../../app/types'; +import { ShortLivingBranch, BranchType, BranchLike, PullRequest } from '../../../../app/types'; import { submit, doAsync, click, waitAndUpdate } from '../../../../helpers/testUtils'; -import { deleteBranch } from '../../../../api/branches'; +import { deleteBranch, deletePullRequest } from '../../../../api/branches'; + +const branch: ShortLivingBranch = { + isMain: false, + name: 'feature', + mergeBranch: 'master', + type: BranchType.SHORT +}; beforeEach(() => { - (deleteBranch as jest.Mock).mockClear(); + (deleteBranch as jest.Mock).mockClear(); + (deletePullRequest as jest.Mock).mockClear(); }); it('renders', () => { - const wrapper = shallowRender(); + const wrapper = shallowRender(branch); expect(wrapper).toMatchSnapshot(); wrapper.setState({ loading: true }); expect(wrapper).toMatchSnapshot(); }); it('deletes branch', async () => { - (deleteBranch as jest.Mock).mockImplementation(() => Promise.resolve()); + (deleteBranch as jest.Mock).mockImplementationOnce(() => Promise.resolve()); + const onDelete = jest.fn(); + const wrapper = shallowRender(branch, onDelete); + + submitForm(wrapper); + + await waitAndUpdate(wrapper); + expect(wrapper.state().loading).toBe(false); + expect(onDelete).toBeCalled(); + expect(deleteBranch).toBeCalledWith({ branch: 'feature', project: 'foo' }); +}); + +it('deletes pull request', async () => { + (deletePullRequest as jest.Mock).mockImplementationOnce(() => Promise.resolve()); + const pullRequest: PullRequest = { + base: 'master', + branch: 'feature', + key: '1234', + title: 'Feature PR' + }; const onDelete = jest.fn(); - const wrapper = shallowRender(onDelete); + const wrapper = shallowRender(pullRequest, onDelete); submitForm(wrapper); await waitAndUpdate(wrapper); expect(wrapper.state().loading).toBe(false); expect(onDelete).toBeCalled(); - expect(deleteBranch).toBeCalledWith('foo', 'feature'); + expect(deletePullRequest).toBeCalledWith({ project: 'foo', pullRequest: '1234' }); }); it('cancels', () => { const onClose = jest.fn(); - const wrapper = shallowRender(jest.fn(), onClose); + const wrapper = shallowRender(branch, jest.fn(), onClose); click(wrapper.find('ResetButtonLink')); @@ -63,27 +93,30 @@ it('cancels', () => { }); it('stops loading on WS error', async () => { - (deleteBranch as jest.Mock).mockImplementation(() => Promise.reject(null)); + (deleteBranch as jest.Mock).mockImplementationOnce(() => Promise.reject(null)); const onDelete = jest.fn(); - const wrapper = shallowRender(onDelete); + const wrapper = shallowRender(branch, onDelete); submitForm(wrapper); await waitAndUpdate(wrapper); expect(wrapper.state().loading).toBe(false); expect(onDelete).not.toBeCalled(); - expect(deleteBranch).toBeCalledWith('foo', 'feature'); + expect(deleteBranch).toBeCalledWith({ branch: 'feature', project: 'foo' }); }); -function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) { - const branch: ShortLivingBranch = { - isMain: false, - name: 'feature', - mergeBranch: 'master', - type: BranchType.SHORT - }; +function shallowRender( + branchLike: BranchLike, + onDelete: () => void = jest.fn(), + onClose: () => void = jest.fn() +) { const wrapper = shallow( - + ); (wrapper.instance() as any).mounted = true; return wrapper; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx index 0e2a0bf65a7..5c19f9f989f 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx @@ -53,7 +53,10 @@ it('opens form', () => { it('fetches setting value on mount', () => { shallow(); - expect(getValues).lastCalledWith('sonar.branch.longLivedBranches.regex', 'project'); + expect(getValues).lastCalledWith({ + keys: 'sonar.branch.longLivedBranches.regex', + component: 'project' + }); }); it('fetches new setting value after change', () => { diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx index 6a107645fc0..4989daa203d 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx @@ -76,7 +76,11 @@ it('resets value', async () => { expect(wrapper).toMatchSnapshot(); click(wrapper.find('Button')); - expect(resetSettingValue).toBeCalledWith('foo', 'project', undefined); + expect(resetSettingValue).toBeCalledWith({ + keys: 'foo', + component: 'project', + branch: undefined + }); await new Promise(setImmediate); expect(onChange).toBeCalled(); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap index 1c14e27cdbe..a9d71179c0c 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap @@ -29,16 +29,27 @@ exports[`renders sorted list of branches 1`] = ` values={ Object { "days": "100", - "settings": - settings.page - , } } /> + +
      + + settings.page + , + } + } + /> +

      + diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap index 5a28f352855..d7e94ca3b2e 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap @@ -4,7 +4,7 @@ exports[`renders main branch 1`] = ` + + + + +`; + exports[`renders short-living branch 1`] = ` { {this.props.displayCoverage && ( { )} { {showIssues && issues.length > 0 && ( void; @@ -49,7 +49,7 @@ export default class LineCoverage extends React.PureComponent { }; render() { - const { branch, componentKey, line, popupOpen } = this.props; + const { branchLike, componentKey, line, popupOpen } = this.props; const className = 'source-meta source-line-coverage' + @@ -80,7 +80,7 @@ export default class LineCoverage extends React.PureComponent { isOpen={popupOpen} popup={ {props.issues.map(issue => ( void; @@ -44,7 +43,7 @@ export default class LineNumber extends React.PureComponent { }; render() { - const { branch, componentKey, line, popupOpen } = this.props; + const { branchLike, componentKey, line, popupOpen } = this.props; const { line: lineNumber } = line; const hasLineNumber = !!lineNumber; return hasLineNumber ? ( @@ -57,7 +56,9 @@ export default class LineNumber extends React.PureComponent { tabIndex={0}> } + popup={ + + } position="bottomright" togglePopup={this.handleTogglePopup} /> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx index db4634b2c28..340304bb280 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx @@ -19,22 +19,22 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import { SourceLine } from '../../../app/types'; +import { BranchLike, SourceLine } from '../../../app/types'; import BubblePopup from '../../common/BubblePopup'; import { translate } from '../../../helpers/l10n'; +import { getBranchLikeQuery } from '../../../helpers/branches'; interface Props { - // TODO use branchLike - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; popupPosition?: any; } -export default function LineOptionsPopup({ branch, componentKey, line, popupPosition }: Props) { +export default function LineOptionsPopup({ branchLike, componentKey, line, popupPosition }: Props) { const permalink = { pathname: '/component', - query: { branch, id: componentKey, line: line.line } + query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) } }; return ( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx index 532e14ff758..11807298299 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx @@ -26,7 +26,7 @@ import { Button } from '../../../components/ui/buttons'; import { getFacets } from '../../../api/issues'; import { getMeasures } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; -import { FacetValue, SourceViewerFile } from '../../../app/types'; +import { FacetValue, SourceViewerFile, BranchLike } from '../../../app/types'; import Modal from '../../controls/Modal'; import Measure from '../../measure/Measure'; import QualifierIcon from '../../shared/QualifierIcon'; @@ -42,10 +42,11 @@ import { getDisplayMetrics, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../helpers/urls'; +import { getBranchLikeQuery } from '../../../helpers/branches'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; onClose: () => void; sourceViewerFile: SourceViewerFile; } @@ -96,23 +97,25 @@ export default class MeasuresOverlay extends React.PureComponent { const metricKeys = getDisplayMetrics(metrics).map(metric => metric.key); // eslint-disable-next-line promise/no-nesting - return getMeasures(this.props.sourceViewerFile.key, metricKeys, this.props.branch).then( - measures => { - const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter( - measure => measure.metric - ); - return keyBy(withMetrics, measure => measure.metric.key); - } - ); + return getMeasures({ + componentKey: this.props.sourceViewerFile.key, + metricKeys: metricKeys.join(), + ...getBranchLikeQuery(this.props.branchLike) + }).then(measures => { + const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter( + measure => measure.metric + ); + return keyBy(withMetrics, measure => measure.metric.key); + }); }); }; fetchIssues = () => { return getFacets( { - branch: this.props.branch, componentKeys: this.props.sourceViewerFile.key, - resolved: 'false' + resolved: 'false', + ...getBranchLikeQuery(this.props.branchLike) }, ['types', 'severities', 'tags'] ).then(({ facets }) => { @@ -383,7 +386,7 @@ export default class MeasuresOverlay extends React.PureComponent { }; render() { - const { branch, sourceViewerFile } = this.props; + const { branchLike, sourceViewerFile } = this.props; const { loading } = this.state; return ( @@ -392,14 +395,14 @@ export default class MeasuresOverlay extends React.PureComponent {
      - + {sourceViewerFile.projectName} {sourceViewerFile.subProject && ( <> - + {sourceViewerFile.subProjectName} @@ -419,7 +422,10 @@ export default class MeasuresOverlay extends React.PureComponent { {sourceViewerFile.q === 'UTS' ? ( <> {this.renderTests()} - + ) : (
      diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx index 64dca78addc..8b2f880be51 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx @@ -23,11 +23,12 @@ import { orderBy } from 'lodash'; import MeasuresOverlayCoveredFiles from './MeasuresOverlayCoveredFiles'; import MeasuresOverlayTestCase from './MeasuresOverlayTestCase'; import { getTests } from '../../../api/tests'; -import { TestCase } from '../../../app/types'; +import { TestCase, BranchLike } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; +import { getBranchLikeQuery } from '../../../helpers/branches'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; } @@ -50,7 +51,7 @@ export default class MeasuresOverlayTestCases extends React.PureComponent { // TODO implement pagination one day... this.setState({ loading: true }); - getTests({ branch: this.props.branch, ps: 500, testFileKey: this.props.componentKey }).then( + getTests({ + ps: 500, + testFileKey: this.props.componentKey, + ...getBranchLikeQuery(this.props.branchLike) + }).then( ({ tests: testCases }) => { if (this.mounted) { this.setState({ loading: false, testCases }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx index a9127eae0ce..c9dc513d817 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import LineCode from '../LineCode'; -import { Issue } from '../../../../app/types'; +import { BranchType, Issue, ShortLivingBranch } from '../../../../app/types'; const issueBase: Issue = { component: '', @@ -50,9 +50,15 @@ it('render code', () => { code: 'class Foo {' }; const issueLocations = [{ from: 0, to: 5, line: 3 }]; + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT + }; const wrapper = shallow( { const line: SourceLine = { line: 3, coverageStatus: 'covered' }; const wrapper = shallow( { const line: SourceLine = { line: 3, coverageStatus: 'uncovered' }; const wrapper = shallow( { const line: SourceLine = { line: 3 }; const wrapper = shallow( { const onPopupToggle = jest.fn(); const wrapper = shallow( { const onIssueClick = jest.fn(); const wrapper = shallow( { const line = { line: 3 }; const wrapper = shallow( { const line = { line: 0 }; const wrapper = shallow( { const line = { line: 3 }; - const wrapper = shallow(); + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT + }; + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx index 1f94f1375d3..3c4a20dd0e2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import MeasuresOverlay from '../MeasuresOverlay'; -import { SourceViewerFile } from '../../../../app/types'; +import { SourceViewerFile, ShortLivingBranch, BranchType } from '../../../../app/types'; import { waitAndUpdate, click } from '../../../../helpers/testUtils'; jest.mock('../../../../api/issues', () => ({ @@ -148,9 +148,20 @@ const sourceViewerFile: SourceViewerFile = { uuid: 'abcd123' }; +const branchLike: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT +}; + it('should render source file', async () => { const wrapper = shallow( - + ); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); @@ -162,7 +173,7 @@ it('should render source file', async () => { it('should render test file', async () => { const wrapper = shallow( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx index 72226708c6f..1eacb341dc0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import MeasuresOverlayTestCases from '../MeasuresOverlayTestCases'; import { waitAndUpdate, click } from '../../../../helpers/testUtils'; +import { ShortLivingBranch, BranchType } from '../../../../app/types'; jest.mock('../../../../api/tests', () => ({ getTests: () => @@ -60,9 +61,16 @@ jest.mock('../../../../api/tests', () => ({ }) })); +const branchLike: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT +}; + it('should render', async () => { const wrapper = shallow( - + ); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap index f5e24cd2736..dc0f5c92200 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap @@ -36,7 +36,14 @@ exports[`render code 1`] = `
      diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap index c578435451a..43ac2de8f8e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap @@ -55,7 +55,6 @@ exports[`should render OK test 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "project:src/file.js", }, } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx index 8354099e6f3..e23ce75a528 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx @@ -18,20 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { searchIssues } from '../../../api/issues'; -import { Issue } from '../../../app/types'; +import { BranchLike, Issue } from '../../../app/types'; +import { getBranchLikeQuery } from '../../../helpers/branches'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { RawQuery } from '../../../helpers/query'; // maximum possible value const PAGE_SIZE = 500; -function buildQuery(component: string, branch: string | undefined) { +function buildQuery(component: string, branchLike: BranchLike | undefined) { return { additionalFields: '_all', resolved: 'false', componentKeys: component, - branch, - s: 'FILE_LINE' + s: 'FILE_LINE', + ...getBranchLikeQuery(branchLike) }; } @@ -75,9 +76,9 @@ export default function loadIssues( component: string, _fromLine: number, toLine: number, - branch: string | undefined + branchLike: BranchLike | undefined ): Promise { - const query = buildQuery(component, branch); + const query = buildQuery(component, branchLike); return new Promise(resolve => { loadPageAndNext(query, toLine, 1).then(issues => { resolve(issues); diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css index dcdb4bfb32d..492b5b861b6 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.css +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.css @@ -18,7 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ .branch-status { - display: flex; + display: inline-flex; + justify-content: flex-end; align-items: center; min-width: 64px; line-height: calc(2 * var(--gridSize)); diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx index 45b271f6179..d2e3f992973 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx @@ -19,27 +19,27 @@ */ import * as React from 'react'; import StatusIndicator from './StatusIndicator'; -import { Branch } from '../../app/types'; import Level from '../ui/Level'; import BugIcon from '../icons-components/BugIcon'; import CodeSmellIcon from '../icons-components/CodeSmellIcon'; import VulnerabilityIcon from '../icons-components/VulnerabilityIcon'; -import { isShortLivingBranch } from '../../helpers/branches'; +import { BranchLike } from '../../app/types'; +import { isShortLivingBranch, isPullRequest, isLongLivingBranch } from '../../helpers/branches'; import './BranchStatus.css'; interface Props { - branch: Branch; + branchLike: BranchLike; concise?: boolean; } -export default function BranchStatus({ branch, concise = false }: Props) { - if (isShortLivingBranch(branch)) { - if (!branch.status) { +export default function BranchStatus({ branchLike, concise = false }: Props) { + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { + if (!branchLike.status) { return null; } const totalIssues = - branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; + branchLike.status.bugs + branchLike.status.vulnerabilities + branchLike.status.codeSmells; const indicatorColor = totalIssues > 0 ? 'red' : 'green'; @@ -56,24 +56,26 @@ export default function BranchStatus({ branch, concise = false }: Props) {
    • - {branch.status.bugs} + {branchLike.status.bugs}
    • - {branch.status.vulnerabilities} + {branchLike.status.vulnerabilities}
    • - {branch.status.codeSmells} + {branchLike.status.codeSmells}
    • ); - } else { - if (!branch.status) { + } else if (isLongLivingBranch(branchLike)) { + if (!branchLike.status) { return null; } - return ; + return ; + } else { + return null; } } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx index 3db595c0a2d..12aef459919 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx @@ -28,14 +28,14 @@ it('renders status of short-living branches', () => { checkShort(7, 3, 6); function checkShort(bugs: number, codeSmells: number, vulnerabilities: number) { - const branch: ShortLivingBranch = { + const shortBranch: ShortLivingBranch = { isMain: false, mergeBranch: 'master', name: 'foo', status: { bugs, codeSmells, vulnerabilities }, type: BranchType.SHORT }; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); } }); @@ -53,6 +53,6 @@ it('renders status of long-living branches', () => { if (qualityGateStatus) { branch.status = { qualityGateStatus }; } - return shallow(); + return shallow(); } }); 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 index 915b6a0fbf5..9ff03913a0d 100644 --- a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx @@ -20,19 +20,21 @@ import * as React from 'react'; import ShortLivingBranchIcon from './ShortLivingBranchIcon'; import LongLivingBranchIcon from './LongLivingBranchIcon'; +import PullRequestIcon from './PullRequestIcon'; import { IconProps } from './types'; -// import PullRequestIcon from './PullRequestIcon'; -import { Branch } from '../../app/types'; -import { isShortLivingBranch } from '../../helpers/branches'; +import { BranchLike } from '../../app/types'; +import { isShortLivingBranch, isPullRequest } from '../../helpers/branches'; interface Props extends IconProps { - branch: Branch; + branchLike: BranchLike; } -export default function BranchIcon({ branch, ...props }: Props) { - return isShortLivingBranch(branch) ? ( - - ) : ( - - ); +export default function BranchIcon({ branchLike, ...props }: Props) { + if (isPullRequest(branchLike)) { + return ; + } else if (isShortLivingBranch(branchLike)) { + return ; + } else { + return ; + } } diff --git a/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx index f0e94ac2fbf..0e7571a54dc 100644 --- a/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx @@ -30,10 +30,11 @@ export default function PullRequestIcon({ className, fill = theme.blue, size = 1 viewBox="0 0 16 16" version="1.1" xmlnsXlink="http://www.w3.org/1999/xlink" - xmlSpace="preserve"> + xmlSpace="preserve" + style={{ fillRule: 'evenodd' }}> ); diff --git a/server/sonar-web/src/main/js/components/issue/Issue.d.ts b/server/sonar-web/src/main/js/components/issue/Issue.d.ts index 6930914f170..389a01a09a2 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.d.ts +++ b/server/sonar-web/src/main/js/components/issue/Issue.d.ts @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Issue as IssueType } from '../../app/types'; +import { BranchLike, Issue as IssueType } from '../../app/types'; interface IssueProps { - branch?: string; + branchLike?: BranchLike; checked?: boolean; displayLocationsCount?: boolean; displayLocationsLink?: boolean; diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js index 47bf09dab76..715d4a2d725 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.js +++ b/server/sonar-web/src/main/js/components/issue/Issue.js @@ -29,7 +29,7 @@ import { setIssueAssignee } from '../../api/issues'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, checked?: boolean, displayLocationsCount?: boolean; displayLocationsLink?: boolean; @@ -150,7 +150,7 @@ export default class Issue extends React.PureComponent { render() { return ( 0; const issueUrl = getComponentIssuesUrl(issue.project, { - branch: props.branch, + ...getBranchLikeQuery(props.branchLike), issues: issue.key, open: issue.key }); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js index f3efe9d2904..9134a0c1f79 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js @@ -48,7 +48,7 @@ const issueWithLocations = { it('should render the titlebar correctly', () => { const element = shallow( { this.context.router.push({ pathname: '/project/activity', - query: { id: this.props.project, branch: this.props.branch } + query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) } }); }; diff --git a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx index 1065e91e9ef..ff5587d95b4 100644 --- a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx +++ b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx @@ -20,6 +20,8 @@ import * as React from 'react'; import { Link } from 'react-router'; import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; +import { BranchLike } from '../../app/types'; +import { getBranchLikeQuery } from '../../helpers/branches'; const ISSUE_MEASURES = [ 'violations', @@ -47,7 +49,7 @@ const ISSUE_MEASURES = [ ]; interface Props { - branch?: string; + branchLike?: BranchLike; children?: React.ReactNode; className?: string; component: string; @@ -121,7 +123,7 @@ export default class DrilldownLink extends React.PureComponent { renderIssuesLink = () => { const url = getComponentIssuesUrl(this.props.component, { ...this.propsToIssueParams(), - branch: this.props.branch + ...getBranchLikeQuery(this.props.branchLike) }); return ( @@ -139,7 +141,7 @@ export default class DrilldownLink extends React.PureComponent { const url = getComponentDrilldownUrl( this.props.component, this.props.metric, - this.props.branch + this.props.branchLike ); return ( diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js index 531db332616..eafcac4460e 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js @@ -49,7 +49,7 @@ export default BaseView.extend({ }, showViewer() { - const { branch, key, line } = this.model.toJSON(); + const { branchLike, key, line } = this.model.toJSON(); const el = document.querySelector(this.viewerRegion.el); @@ -57,7 +57,7 @@ export default BaseView.extend({ { it('sorts main branch and short-living branches', () => { const main = mainBranch(); - const foo = shortLivingBranch('foo', 'master'); - const bar = shortLivingBranch('bar', 'master'); + const foo = shortLivingBranch({ name: 'foo' }); + const bar = shortLivingBranch({ name: 'bar' }); expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); }); it('sorts main branch and long-living branches', () => { const main = mainBranch(); - const foo = longLivingBranch('foo'); - const bar = longLivingBranch('bar'); + const foo = longLivingBranch({ name: 'foo' }); + const bar = longLivingBranch({ name: 'bar' }); expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); }); it('sorts all types of branches', () => { const main = mainBranch(); - const shortFoo = shortLivingBranch('shortFoo', 'master'); - const shortBar = shortLivingBranch('shortBar', 'longBaz'); - const shortPre = shortLivingBranch('shortPre', 'shortFoo'); - const longBaz = longLivingBranch('longBaz'); - const longQux = longLivingBranch('longQux'); - const longQwe = longLivingBranch('longQwe'); + const shortFoo = shortLivingBranch({ name: 'shortFoo', mergeBranch: 'master' }); + const shortBar = shortLivingBranch({ name: 'shortBar', mergeBranch: 'longBaz' }); + const shortPre = shortLivingBranch({ name: 'shortPre', mergeBranch: 'shortFoo' }); + const longBaz = longLivingBranch({ name: 'longBaz' }); + const longQux = longLivingBranch({ name: 'longQux' }); + const longQwe = longLivingBranch({ name: 'longQwe' }); + const pr = pullRequest({ base: 'master' }); // - main - main // - shortFoo - shortFoo // - shortPre - shortPre @@ -51,8 +58,43 @@ describe('#sortBranchesAsTree', () => { // - longQwe - longQwe // - longQux - longQux expect( - sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe]) - ).toEqual([main, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]); + sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe, pr]) + ).toEqual([main, pr, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]); + }); +}); + +describe('#isSameBranchLike', () => { + it('compares different kinds', () => { + const main = mainBranch(); + const short = shortLivingBranch({ name: 'foo' }); + const long = longLivingBranch({ name: 'foo' }); + const pr = pullRequest(); + expect(isSameBranchLike(main, pr)).toBeFalsy(); + expect(isSameBranchLike(main, short)).toBeFalsy(); + expect(isSameBranchLike(main, long)).toBeFalsy(); + expect(isSameBranchLike(pr, short)).toBeFalsy(); + expect(isSameBranchLike(pr, long)).toBeFalsy(); + expect(isSameBranchLike(short, long)).toBeFalsy(); + }); + + it('compares pull requests', () => { + expect(isSameBranchLike(pullRequest({ key: '1234' }), pullRequest({ key: '1234' }))).toBeTruthy(); + expect(isSameBranchLike(pullRequest({ key: '1234' }), pullRequest({ key: '5678' }))).toBeFalsy(); + }); + + it('compares branches', () => { + expect( + isSameBranchLike(longLivingBranch({ name: 'foo' }), longLivingBranch({ name: 'foo' })) + ).toBeTruthy(); + expect( + isSameBranchLike(shortLivingBranch({ name: 'foo' }), shortLivingBranch({ name: 'foo' })) + ).toBeTruthy(); + expect( + isSameBranchLike(longLivingBranch({ name: 'foo' }), longLivingBranch({ name: 'bar' })) + ).toBeFalsy(); + expect( + isSameBranchLike(shortLivingBranch({ name: 'foo' }), shortLivingBranch({ name: 'bar' })) + ).toBeFalsy(); }); }); @@ -60,12 +102,31 @@ function mainBranch(): MainBranch { return { isMain: true, name: 'master' }; } -function shortLivingBranch(name: string, mergeBranch: string): ShortLivingBranch { +function shortLivingBranch(overrides?: Partial): ShortLivingBranch { const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 }; - return { isMain: false, mergeBranch, name, status, type: BranchType.SHORT }; + return { + isMain: false, + mergeBranch: 'master', + name: 'foo', + status, + type: BranchType.SHORT, + ...overrides + }; } -function longLivingBranch(name: string): LongLivingBranch { +function longLivingBranch(overrides?: Partial): LongLivingBranch { const status = { qualityGateStatus: 'OK' }; - return { isMain: false, name, status, type: BranchType.LONG }; + return { isMain: false, name: 'foo', status, type: BranchType.LONG, ...overrides }; +} + +function pullRequest(overrides?: Partial): PullRequest { + const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 }; + return { + base: 'master', + branch: 'feature', + key: '1234', + status, + title: 'Random Name', + ...overrides + }; } diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts index ecd54ab1adb..75382073d32 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts @@ -44,8 +44,8 @@ afterEach(() => { describe('#getPathUrlAsString', () => { it('should return component url', () => { - expect(getPathUrlAsString(getProjectUrl(SIMPLE_COMPONENT_KEY, 'branch:7.0'))).toBe( - '/dashboard?id=' + SIMPLE_COMPONENT_KEY + '&branch=branch%3A7.0' + expect(getPathUrlAsString(getProjectUrl(SIMPLE_COMPONENT_KEY))).toBe( + '/dashboard?id=' + SIMPLE_COMPONENT_KEY ); }); diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts index 195b5a95a0e..510ed8c3f85 100644 --- a/server/sonar-web/src/main/js/helpers/branches.ts +++ b/server/sonar-web/src/main/js/helpers/branches.ts @@ -18,43 +18,116 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { sortBy } from 'lodash'; -import { Branch, BranchType, ShortLivingBranch, LongLivingBranch } from '../app/types'; +import { + BranchLike, + Branch, + BranchType, + ShortLivingBranch, + LongLivingBranch, + PullRequest, + MainBranch, + BranchParameters +} from '../app/types'; -export function isShortLivingBranch(branch?: Branch): branch is ShortLivingBranch { - return branch !== undefined && !branch.isMain && branch.type === BranchType.SHORT; +export function isBranch(branchLike?: BranchLike): branchLike is Branch { + return branchLike !== undefined && (branchLike as Branch).isMain !== undefined; } -export function isLongLivingBranch(branch?: Branch): branch is LongLivingBranch { - return branch !== undefined && !branch.isMain && branch.type === BranchType.LONG; +export function isShortLivingBranch(branchLike?: BranchLike): branchLike is ShortLivingBranch { + return ( + isBranch(branchLike) && + !branchLike.isMain && + (branchLike as ShortLivingBranch).type === BranchType.SHORT + ); } -export function getBranchName(branch?: Branch): string | undefined { - return !branch || branch.isMain ? undefined : branch.name; +export function isLongLivingBranch(branchLike?: BranchLike): branchLike is LongLivingBranch { + return ( + isBranch(branchLike) && + !branchLike.isMain && + (branchLike as LongLivingBranch).type === BranchType.LONG + ); } -export function sortBranchesAsTree(branches: Branch[]): Branch[] { - const result: Branch[] = []; +export function isMainBranch(branchLike?: BranchLike): branchLike is MainBranch { + return isBranch(branchLike) && branchLike.isMain; +} + +export function isPullRequest(branchLike?: BranchLike): branchLike is PullRequest { + return branchLike !== undefined && (branchLike as PullRequest).key !== undefined; +} - const shortLivingBranches = branches.filter(isShortLivingBranch); +export function getPullRequestDisplayName(pullRequest: PullRequest) { + return `${pullRequest.key} – ${pullRequest.title}`; +} + +export function getBranchLikeDisplayName(branchLike: BranchLike) { + return isPullRequest(branchLike) ? getPullRequestDisplayName(branchLike) : branchLike.name; +} + +export function getBranchLikeKey(branchLike: BranchLike) { + return isPullRequest(branchLike) ? `pull-request-${branchLike.key}` : `branch-${branchLike.name}`; +} + +export function isSameBranchLike(a: BranchLike | undefined, b: BranchLike | undefined) { + // main branches are always equal + if (isMainBranch(a) && isMainBranch(b)) { + return true; + } + + // short- and long-living branches are compared by type and name + if ( + (isLongLivingBranch(a) && isLongLivingBranch(b)) || + (isShortLivingBranch(a) && isShortLivingBranch(b)) + ) { + return a.type === b.type && a.name === b.name; + } + + // pull requests are compared by id + if (isPullRequest(a) && isPullRequest(b)) { + return a.key === b.key; + } + + // finally if both parameters are `undefined`, consider them equal + return a === b; +} + +export function sortBranchesAsTree(branchLikes: BranchLike[]) { + const result: BranchLike[] = []; + + const mainBranch = branchLikes.find(isMainBranch); + const longLivingBranches = branchLikes.filter(isLongLivingBranch); + const shortLivingBranches = branchLikes.filter(isShortLivingBranch); + const pullRequests = branchLikes.filter(isPullRequest); // main branch is always first - const mainBranch = branches.find(branch => branch.isMain); if (mainBranch) { - result.push(mainBranch, ...getNestedShortLivingBranches(mainBranch.name)); + result.push( + mainBranch, + ...getPullRequests(mainBranch.name), + ...getNestedShortLivingBranches(mainBranch.name) + ); } - // the all long-living branches - sortBy(branches.filter(isLongLivingBranch), 'name').forEach(longLivingBranch => { - result.push(longLivingBranch, ...getNestedShortLivingBranches(longLivingBranch.name)); + // then all long-living branches + sortBy(longLivingBranches, 'name').forEach(longLivingBranch => { + result.push( + longLivingBranch, + ...getPullRequests(longLivingBranch.name), + ...getNestedShortLivingBranches(longLivingBranch.name) + ); }); - // finally all orhpan branches - result.push(...shortLivingBranches.filter(branch => branch.isOrphan)); + // finally all orhpan pull requests and branches + result.push( + ...pullRequests.filter(pr => pr.isOrphan), + ...shortLivingBranches.filter(branch => branch.isOrphan) + ); return result; /** Get all short-living branches (possibly nested) which should be merged to a given branch */ - function getNestedShortLivingBranches(mergeBranch: string): ShortLivingBranch[] { + function getNestedShortLivingBranches(mergeBranch: string) { const found: ShortLivingBranch[] = shortLivingBranches.filter( branch => branch.mergeBranch === mergeBranch ); @@ -68,4 +141,18 @@ export function sortBranchesAsTree(branches: Branch[]): Branch[] { return sortBy(found, 'name'); } + + function getPullRequests(base: string) { + return pullRequests.filter(pr => pr.base === base); + } +} + +export function getBranchLikeQuery(branchLike?: BranchLike): BranchParameters { + if (isShortLivingBranch(branchLike) || isLongLivingBranch(branchLike)) { + return { branch: branchLike.name }; + } else if (isPullRequest(branchLike)) { + return { pullRequest: branchLike.key }; + } else { + return {}; + } } diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index ae125724761..2f6b3140840 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -19,9 +19,14 @@ */ import { stringify } from 'querystring'; import { omitBy, isNil } from 'lodash'; -import { isShortLivingBranch } from './branches'; +import { + isShortLivingBranch, + isPullRequest, + isLongLivingBranch, + getBranchLikeQuery +} from './branches'; import { getProfilePath } from '../apps/quality-profiles/utils'; -import { Branch, HomePage, HomePageType } from '../app/types'; +import { BranchLike, HomePage, HomePageType } from '../app/types'; interface Query { [x: string]: string | undefined; @@ -44,8 +49,8 @@ export function getPathUrlAsString(path: Location): string { return `${getBaseUrl()}${path.pathname}?${stringify(omitBy(path.query, isNil))}`; } -export function getProjectUrl(key: string, branch?: string): Location { - return { pathname: '/dashboard', query: { id: key, branch } }; +export function getProjectUrl(project: string): Location { + return { pathname: '/dashboard', query: { id: project } }; } export function getPortfolioUrl(key: string): Location { @@ -56,19 +61,30 @@ export function getComponentBackgroundTaskUrl(componentKey: string, status?: str return { pathname: '/project/background_tasks', query: { id: componentKey, status } }; } -export function getProjectBranchUrl(key: string, branch: Branch): Location { - if (isShortLivingBranch(branch)) { - return { - pathname: '/project/issues', - query: { branch: branch.name, id: key, resolved: 'false' } - }; - } else if (!branch.isMain) { - return { pathname: '/dashboard', query: { branch: branch.name, id: key } }; +export function getBranchLikeUrl(project: string, branchLike?: BranchLike): Location { + if (isPullRequest(branchLike)) { + return getPullRequestUrl(project, branchLike.key); + } else if (isShortLivingBranch(branchLike)) { + return getShortLivingBranchUrl(project, branchLike.name); + } else if (isLongLivingBranch(branchLike)) { + return getLongLivingBranchUrl(project, branchLike.name); } else { - return { pathname: '/dashboard', query: { id: key } }; + return getProjectUrl(project); } } +export function getLongLivingBranchUrl(project: string, branch: string): Location { + return { pathname: '/dashboard', query: { branch, id: project } }; +} + +export function getShortLivingBranchUrl(project: string, branch: string): Location { + return { pathname: '/project/issues', query: { branch, id: project, resolved: 'false' } }; +} + +export function getPullRequestUrl(project: string, pullRequest: string): Location { + return { pathname: '/project/issues', query: { id: project, pullRequest, resolved: 'false' } }; +} + /** * Generate URL for a global issues page */ @@ -90,25 +106,40 @@ export function getComponentIssuesUrl(componentKey: string, query?: Query): Loca export function getComponentDrilldownUrl( componentKey: string, metric: string, - branch?: string + branchLike?: BranchLike ): Location { - return { pathname: '/component_measures', query: { id: componentKey, metric, branch } }; + return { + pathname: '/component_measures', + query: { id: componentKey, metric, ...getBranchLikeQuery(branchLike) } + }; } -export function getMeasureTreemapUrl(component: string, metric: string, branch?: string) { +export function getMeasureTreemapUrl(component: string, metric: string) { return { pathname: '/component_measures', - query: { id: component, metric, branch, view: 'treemap' } + query: { id: component, metric, view: 'treemap' } + }; +} + +export function getActivityUrl(component: string, branchLike?: BranchLike) { + return { + pathname: '/project/activity', + query: { id: component, ...getBranchLikeQuery(branchLike) } }; } /** * Generate URL for a component's measure history */ -export function getMeasureHistoryUrl(component: string, metric: string, branch?: string) { +export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) { return { pathname: '/project/activity', - query: { id: component, graph: 'custom', custom_metrics: metric, branch } + query: { + id: component, + graph: 'custom', + custom_metrics: metric, + ...getBranchLikeQuery(branchLike) + } }; } @@ -172,8 +203,8 @@ export function getMarkdownHelpUrl(): string { return getBaseUrl() + '/markdown/help'; } -export function getCodeUrl(project: string, branch?: string, selected?: string) { - return { pathname: '/code', query: { id: project, branch, selected } }; +export function getCodeUrl(project: string, branchLike?: BranchLike, selected?: string) { + return { pathname: '/code', query: { id: project, ...getBranchLikeQuery(branchLike), selected } }; } export function getOrganizationUrl(organization: string) { @@ -185,7 +216,9 @@ export function getHomePageUrl(homepage: HomePage) { case HomePageType.Application: return getProjectUrl(homepage.component); case HomePageType.Project: - return getProjectUrl(homepage.component, homepage.branch); + return homepage.branch + ? getLongLivingBranchUrl(homepage.component, homepage.branch) + : getProjectUrl(homepage.component); case HomePageType.Organization: return getOrganizationUrl(homepage.organization); case HomePageType.Portfolio: diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 43d19d28015..f5b5fb14f02 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -489,10 +489,10 @@ deletion.page=Deletion project_deletion.page.description=Delete this project. The operation cannot be undone. portfolio_deletion.page.description=Delete this portfolio. Component projects and Local Reference Portfolios will not be deleted, but component Standard Portfolios will be deleted. This operation cannot be undone. application_deletion.page.description=Delete this application. Application projects will not be deleted. This operation cannot be undone. -project_branches.page=Branches -project_branches.page.description=Use this page to manage project branches. -project_branches.page.life_time=Short-lived branches are permanently deleted after {days} days without analysis. -project_branches.page.life_time.admin=Short-lived branches are permanently deleted after {days} days without analysis. You can adjust this value globally in {settings}. +project_branches.page=Branches & Pull Requests +project_branches.page.description=Use this page to manage project branches and pull requests. +project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis. +project_branches.page.life_time.admin=You can adjust this value globally in {settings}. #------------------------------------------------------------------------------ # @@ -2632,11 +2632,13 @@ branches.learn_how_to_analyze=Learn how to analyze branches in SonarQube branches.learn_how_to_analyze.text=Quickly setup branch analysis and get separate insights for each of your branches and pull requests. branches.delete=Delete Branch branches.delete.are_you_sure=Are you sure you want to delete branch "{0}"? +branches.pull_request.delete=Delete Pull Request +branches.pull_request.delete.are_you_sure=Are you sure you want to delete pull request "{0}"? branches.rename=Rename Branch branches.manage=Manage branches branches.orphan_branch=Orphan Branch -branches.orphan_branches=Orphan Branches -branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan. +branches.orphan_branches=Orphan Branches & Pull Requests +branches.orphan_branches.tooltip=When a target branch of a short-living branch or a base of a pull request was deleted, this short-living branch or pull request becomes orphan. branches.main_branch=Main Branch branches.branch_settings=Branch Settings branches.long_living_branches_pattern=Long living branches pattern @@ -2647,6 +2649,10 @@ branches.last_analysis_date=Last Analysis Date branches.no_support.header=Get the most out of SonarQube with branches analysis branches.no_support.header.text=Analyze each branch of your project separately with the Developer Edition. branches.search_for_branches=Search for branches... +branches.pull_requests=Pull Requests +branches.short_lived_branches=Short-lived branches +branches.pull_request.for_merge_into_x_from_y=for merge into {base} from {branch} +branches.see_the_pr=See the PR #------------------------------------------------------------------------------
      + {/* TODO make this link a react-router */} {component.refKey == null ? ( {this.renderInner()} @@ -81,7 +82,7 @@ export default class ComponentCell extends React.PureComponent { + to={getBranchLikeUrl(component.refKey, branchLike)}> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js index f2b45b73878..0e29a704f0c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js @@ -27,7 +27,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, components: Array, onClick: string => void, metric: Metric, @@ -36,7 +36,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; |}; */ export default function ComponentsList( - { branch, components, onClick, metrics, metric, selectedComponent } /*: Props */ + { branchLike, components, onClick, metrics, metric, selectedComponent } /*: Props */ ) { if (!components.length) { return ; @@ -65,7 +65,7 @@ export default function ComponentsList( {components.map(component => ( void, @@ -35,7 +35,7 @@ import MeasureCell from './MeasureCell'; |}; */ export default function ComponentsListRow(props /*: Props */) { - const { branch, component } = props; + const { branchLike, component } = props; const otherMeasures = props.otherMetrics.map(metric => { const measure = component.measures.find(measure => measure.metric.key === metric.key); return { ...measure, metric }; @@ -45,7 +45,7 @@ export default function ComponentsListRow(props /*: Props */) { }); return (
      - {isLongLivingBranch(branch) && ( + {isLongLivingBranch(branchLike) && ( {translate('branches.set_leak_period')} )} - {isLongLivingBranch(branch) && !branch.isMain && } - {branch.isMain ? ( + {isLongLivingBranch(branchLike) && } + {isMainBranch(branchLike) ? ( {translate('branches.rename')} @@ -112,62 +119,65 @@ export default class BranchRow extends React.PureComponent { className="js-delete" destructive={true} onClick={this.handleDeleteClick}> - {translate('branches.delete')} + {translate( + isPullRequest(branchLike) ? 'branches.pull_request.delete' : 'branches.delete' + )} )} {this.state.deleting && ( )} - {this.state.renaming && ( - - )} + {this.state.renaming && + isMainBranch(branchLike) && ( + + )} - {this.state.changingLeak && ( - - )} + {this.state.changingLeak && + isLongLivingBranch(branchLike) && ( + + )}
      - {branch.name} - {branch.isMain && ( + {getBranchLikeDisplayName(branchLike)} + {isMainBranch(branchLike) && (
      {translate('branches.main_branch')}
      )}
      - + - {branch.analysisDate && } + {branchLike.analysisDate && }
      `; +exports[`renders pull request 1`] = ` +
      + + 1234 – Feature PR + + + + + + + branches.pull_request.delete + + +
      // call `getComponentNavigation` to check if user has the "Administer" permission // call `getComponentShow` to check if user has the "Browse" permission Promise.all([ - getComponentNavigation(this.props.project.key), - getComponentShow(this.props.project.key) + getComponentNavigation({ componentKey: this.props.project.key }), + getComponentShow({ component: this.props.project.key }) ]).then( ([navResponse]) => { if (this.mounted) { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap index deec1e2e7fd..b0b662adaae 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap @@ -385,7 +385,6 @@ exports[`creates project 4`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "name", }, } diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.js b/server/sonar-web/src/main/js/apps/settings/store/actions.js index 7865fd56313..dcbf19f63dd 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/actions.js +++ b/server/sonar-web/src/main/js/apps/settings/store/actions.js @@ -50,10 +50,10 @@ export const fetchSettings = componentKey => dispatch => { ); }; -export const fetchValues = (keys, componentKey) => dispatch => - getValues(keys, componentKey).then( +export const fetchValues = (keys, component) => dispatch => + getValues({ keys, component }).then( settings => { - dispatch(receiveValues(settings, componentKey)); + dispatch(receiveValues(settings, component)); dispatch(closeAllGlobalMessages()); }, () => {} @@ -73,7 +73,7 @@ export const saveValue = (key, componentKey) => (dispatch, getState) => { } return setSettingValue(definition, value, componentKey) - .then(() => getValues(key, componentKey)) + .then(() => getValues({ keys: key, component: componentKey })) .then(values => { dispatch(receiveValues(values, componentKey)); dispatch(cancelChange(key)); @@ -90,8 +90,8 @@ export const saveValue = (key, componentKey) => (dispatch, getState) => { export const resetValue = (key, componentKey) => dispatch => { dispatch(startLoading(key)); - return resetSettingValue(key, componentKey) - .then(() => getValues(key, componentKey)) + return resetSettingValue({ keys: key, component: componentKey }) + .then(() => getValues({ keys: key, component: componentKey })) .then(values => { if (values.length > 0) { dispatch(receiveValues(values, componentKey)); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx index 6e487e2558a..101bc211a1a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx @@ -38,14 +38,16 @@ import { getSources } from '../../api/components'; import { + BranchLike, + DuplicatedFile, Duplication, FlowLocation, Issue, LinearIssueLocation, SourceLine, - SourceViewerFile, - DuplicatedFile + SourceViewerFile } from '../../app/types'; +import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches'; import { parseDate } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; import './styles.css'; @@ -54,7 +56,7 @@ import './styles.css'; interface Props { aroundLine?: number; - branch: string | undefined; + branchLike: BranchLike | undefined; component: string; displayAllIssues?: boolean; displayIssueLocationsCount?: boolean; @@ -63,18 +65,21 @@ interface Props { highlightedLine?: number; highlightedLocations?: FlowLocation[]; highlightedLocationMessage?: { index: number; text: string }; - loadComponent?: (component: string, branch: string | undefined) => Promise; + loadComponent?: ( + component: string, + branchLike: BranchLike | undefined + ) => Promise; loadIssues?: ( component: string, from: number, to: number, - branch: string | undefined + branchLike: BranchLike | undefined ) => Promise; loadSources?: ( component: string, from: number, to: number, - branch: string | undefined + branchLike: BranchLike | undefined ) => Promise; onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; onLocationSelect?: (index: number) => void; @@ -162,7 +167,10 @@ export default class SourceViewerBase extends React.PureComponent } componentDidUpdate(prevProps: Props) { - if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { + if ( + prevProps.component !== this.props.component || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) + ) { this.fetchComponent(); } else if ( this.props.aroundLine !== undefined && @@ -220,7 +228,7 @@ export default class SourceViewerBase extends React.PureComponent fetchComponent() { this.setState({ loading: true }); const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => { - this.safeLoadIssues(this.props.component, 1, LINES, this.props.branch).then( + this.safeLoadIssues(this.props.component, 1, LINES, this.props.branchLike).then( issues => { if (this.mounted) { const finalSources = sources.slice(0, LINES); @@ -284,7 +292,7 @@ export default class SourceViewerBase extends React.PureComponent ); }; - this.safeLoadComponent(this.props.component, this.props.branch).then( + this.safeLoadComponent(this.props.component, this.props.branchLike).then( onResolve, onFailLoadComponent ); @@ -324,7 +332,7 @@ export default class SourceViewerBase extends React.PureComponent this.props.component, firstSourceLine && firstSourceLine.line, lastSourceLine && lastSourceLine.line, - this.props.branch + this.props.branchLike ).then( issues => { if (this.mounted) { @@ -364,7 +372,7 @@ export default class SourceViewerBase extends React.PureComponent // request one additional line to define `hasSourcesAfter` to++; - return this.safeLoadSources(this.props.component, from, to, this.props.branch).then( + return this.safeLoadSources(this.props.component, from, to, this.props.branchLike).then( sources => resolve(sources), onFailLoadSources ); @@ -382,14 +390,14 @@ export default class SourceViewerBase extends React.PureComponent this.props.component, from, firstSourceLine.line - 1, - this.props.branch + this.props.branchLike ).then( sources => { this.safeLoadIssues( this.props.component, from, firstSourceLine.line - 1, - this.props.branch + this.props.branchLike ).then( issues => { if (this.mounted) { @@ -429,9 +437,9 @@ 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.safeLoadSources(this.props.component, fromLine, toLine, this.props.branch).then( + this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branchLike).then( sources => { - this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branch).then( + this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branchLike).then( issues => { if (this.mounted) { this.setState(prevState => { @@ -469,7 +477,10 @@ export default class SourceViewerBase extends React.PureComponent }; loadDuplications = (line: SourceLine) => { - getDuplications(this.props.component, this.props.branch).then( + getDuplications({ + key: this.props.component, + ...getBranchLikeQuery(this.props.branchLike) + }).then( r => { if (this.mounted) { this.setState(() => { @@ -615,7 +626,7 @@ export default class SourceViewerBase extends React.PureComponent return ( const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; return ( return (
      (this.node = node)}> {this.state.component && ( - + )} {sourceRemoved && (
      @@ -719,10 +733,10 @@ export default class SourceViewerBase extends React.PureComponent } } -function defaultLoadComponent(key: string, branch: string | undefined) { +function defaultLoadComponent(key: string, branchLike: BranchLike | undefined) { return Promise.all([ - getComponentForSourceViewer(key, branch), - getComponentData(key, branch) + getComponentForSourceViewer({ component: key, ...getBranchLikeQuery(branchLike) }), + getComponentData({ component: key, ...getBranchLikeQuery(branchLike) }) ]).then(([component, data]) => ({ ...component, leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate) @@ -733,7 +747,7 @@ function defaultLoadSources( key: string, from: number | undefined, to: number | undefined, - branch: string | undefined + branchLike: BranchLike | undefined ) { - return getSources(key, from, to, branch); + return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) }); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx index 15db6dd0961..96bd3512e39 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx @@ -21,7 +21,14 @@ import * as React from 'react'; import { intersection } from 'lodash'; import Line from './components/Line'; import { getLinearLocations } from './helpers/issueLocations'; -import { Duplication, FlowLocation, Issue, LinearIssueLocation, SourceLine } from '../../app/types'; +import { + BranchLike, + Duplication, + FlowLocation, + Issue, + LinearIssueLocation, + SourceLine +} from '../../app/types'; import { translate } from '../../helpers/l10n'; import { Button } from '../ui/buttons'; @@ -34,7 +41,7 @@ const ZERO_LINE = { }; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; displayAllIssues?: boolean; displayIssueLocationsCount?: boolean; @@ -153,7 +160,7 @@ export default class SourceViewerCode extends React.PureComponent { return ( + href={getPathUrlAsString(getBranchLikeUrl(project, this.props.branchLike))}> {projectName}
      @@ -98,7 +100,7 @@ export default class SourceViewerHeader extends React.PureComponent + href={getPathUrlAsString(getBranchLikeUrl(subProject, this.props.branchLike))}> {subProjectName}
      @@ -127,7 +129,7 @@ export default class SourceViewerHeader extends React.PureComponent {this.state.measuresOverlay && ( @@ -138,7 +140,7 @@ export default class SourceViewerHeader extends React.PureComponent {translate('component_viewer.new_window')} @@ -188,7 +190,7 @@ export default class SourceViewerHeader extends React.PureComponent {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx index 2edff725967..ef06788347e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx @@ -20,14 +20,15 @@ import * as React from 'react'; import { groupBy } from 'lodash'; import { getTests } from '../../../api/components'; -import { SourceLine, TestCase } from '../../../app/types'; +import { BranchLike, SourceLine, TestCase } from '../../../app/types'; import BubblePopup from '../../common/BubblePopup'; import TestStatusIcon from '../../shared/TestStatusIcon'; +import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; import { collapsePath } from '../../../helpers/path'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; onClose: () => void; @@ -49,9 +50,8 @@ export default class CoveragePopup extends React.PureComponent { } componentDidUpdate(prevProps: Props) { - // TODO use branchLike if ( - prevProps.branch !== this.props.branch || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || prevProps.componentKey !== this.props.componentKey || prevProps.line.line !== this.props.line.line ) { @@ -65,7 +65,11 @@ export default class CoveragePopup extends React.PureComponent { fetchTests = () => { this.setState({ loading: true }); - getTests(this.props.componentKey, this.props.line.line, this.props.branch).then( + getTests({ + sourceFileKey: this.props.componentKey, + sourceFileLineNumber: this.props.line.line, + ...getBranchLikeQuery(this.props.branchLike) + }).then( testCases => { if (this.mounted) { this.setState({ loading: false, testCases }); @@ -84,7 +88,7 @@ export default class CoveragePopup extends React.PureComponent { event.currentTarget.blur(); const { key } = event.currentTarget.dataset; const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, branch: this.props.branch }); + Workspace.openComponent({ key, branchLike: this.props.branchLike }); this.props.onClose(); }; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx index 9c58a3e01ad..d4b75a93b28 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Link } from 'react-router'; import { groupBy, sortBy } from 'lodash'; -import { SourceViewerFile, DuplicationBlock, DuplicatedFile } from '../../../app/types'; +import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types'; import BubblePopup from '../../common/BubblePopup'; import QualifierIcon from '../../shared/QualifierIcon'; import { translate } from '../../../helpers/l10n'; @@ -29,8 +29,7 @@ import { getProjectUrl } from '../../../helpers/urls'; interface Props { blocks: DuplicationBlock[]; - // TODO use branchLike - branch: string | undefined; + branchLike: BranchLike | undefined; duplicatedFiles?: { [ref: string]: DuplicatedFile }; inRemovedComponent: boolean; onClose: () => void; @@ -51,7 +50,7 @@ export default class DuplicationPopup extends React.PureComponent { event.currentTarget.blur(); const Workspace = require('../../workspace/main').default; const { key, line } = event.currentTarget.dataset; - Workspace.openComponent({ key, line, branch: this.props.branch }); + Workspace.openComponent({ key, line, branchLike: this.props.branchLike }); this.props.onClose(); }; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx index a29255557dd..c6fef8178b3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -27,10 +27,10 @@ import LineDuplications from './LineDuplications'; import LineDuplicationBlock from './LineDuplicationBlock'; import LineIssuesIndicator from './LineIssuesIndicator'; import LineCode from './LineCode'; -import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; +import { BranchLike, Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; displayAllIssues?: boolean; displayCoverage: boolean; @@ -121,7 +121,7 @@ export default class Line extends React.PureComponent { return (