diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2018-03-12 12:06:11 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk@users.noreply.github.com> | 2018-03-13 14:05:36 +0100 |
commit | 913c82c8772fd4747626a1fbe665ccda2e5ca9f1 (patch) | |
tree | d48784851df80905ce125cc60ac8aec8570751a9 | |
parent | 751e4000e40a4af66b80767d632b1bef64dc5647 (diff) | |
download | sonarqube-913c82c8772fd4747626a1fbe665ccda2e5ca9f1.tar.gz sonarqube-913c82c8772fd4747626a1fbe665ccda2e5ca9f1.zip |
SONAR-10374 Support pull request in the web app
175 files changed, 2135 insertions, 1242 deletions
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<any> { +export function getBranches(project: string): Promise<Branch[]> { return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); } -export function deleteBranch(project: string, branch: string): Promise<void | Response> { - return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError); +export function getPullRequests(project: string): Promise<PullRequest[]> { + return getJSON('/api/project_pull_requests/list', { project }).then( + r => r.pullRequests, + throwGlobalError + ); } -export function renameBranch(project: string, name: string): Promise<void | Response> { +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<any> { - 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<a return getJSON('/api/components/tree', { ...options, component }); } -export function getComponentShow(component: string, branch?: string): Promise<any> { - return getJSON('/api/components/show', { component, branch }); +export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> { + return getJSON('/api/components/show', data); } export function getParents(component: string): Promise<any> { - return getComponentShow(component).then(r => r.ancestors); + return getComponentShow({ component }).then(r => r.ancestors); } -export function getBreadcrumbs(component: string, branch?: string): Promise<any> { - return getComponentShow(component, branch).then(r => { +export function getBreadcrumbs(data: { component: string } & BranchParameters): Promise<any> { + return getComponentShow(data).then(r => { const reversedAncestors = [...r.ancestors].reverse(); return [...reversedAncestors, r.component]; }); } -export function getComponentData(component: string, branch?: string): Promise<any> { - return getComponentShow(component, branch).then(r => r.component); +export function getComponentData(data: { component: string } & BranchParameters): Promise<any> { + return getComponentShow(data).then(r => r.component); } export function getMyProjects(data: RequestData): Promise<any> { @@ -246,31 +243,24 @@ export function getSuggestions( return getJSON('/api/components/suggestions', data); } -export function getComponentForSourceViewer(component: string, branch?: string): Promise<any> { - return getJSON('/api/components/app', { component, branch }); +export function getComponentForSourceViewer( + data: { component: string } & BranchParameters +): Promise<any> { + 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<any> { - 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<any> { - return getJSON('/api/duplications/show', { key: component, branch }); +export function getDuplications(data: { key: string } & BranchParameters): Promise<any> { + return getJSON('/api/duplications/show', data); } -export function getTests(component: string, line: number | string, branch?: string): Promise<any> { - const data = { sourceFileKey: component, sourceFileLineNumber: line, branch }; +export function getTests( + data: { sourceFileKey: string; sourceFileLineNumber: number | string } & BranchParameters +): Promise<any> { 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<any> { return getJSON('/api/navigation/global'); } -export function getComponentNavigation(componentKey: string, branch?: string): Promise<any> { - return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError); +export function getComponentNavigation( + data: { componentKey: string } & BranchParameters +): Promise<any> { + return getJSON('/api/navigation/component', data).catch(throwGlobalError); } export function getSettingsNavigation(): Promise<any> { 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<any> { - return getJSON('/api/settings/list_definitions', { branch, component }).then(r => r.definitions); +export function getDefinitions(component?: string): Promise<any> { + 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<SettingValue[]> { - 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<void> { +export function setSettingValue(definition: any, value: any, component?: string): Promise<void> { 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<void | Response> { - return post('/api/settings/set', parameters).catch(throwGlobalError); +export function setSimpleSettingValue( + data: { component?: string; value: string; key: string } & BranchParameters +): Promise<void | Response> { + return post('/api/settings/set', data).catch(throwGlobalError); } -export function resetSettingValue(key: string, component?: string, branch?: string): Promise<void> { - return post('/api/settings/reset', { keys: key, component, branch }); +export function resetSettingValue( + data: { keys: string; component?: string } & BranchParameters +): Promise<void> { + return post('/api/settings/reset', data); } export function sendTestEmail(to: string, subject: string, message: string): Promise<void> { 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<TimeMachineResponse> { - 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<string>, - other?: { branch?: string; p?: number; from?: string; to?: string }, + data: { + component: string; + metrics: string; + from?: string; + p?: number; + to?: string; + } & BranchParameters, prev?: TimeMachineResponse ): Promise<TimeMachineResponse> { - 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<Props, State> { 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<Props, State> { 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<Props, State> { }); 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<Props, State> { } }; - 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<BranchLike[]> => { 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<Props, State> { 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<Props, State> { render() { const { query } = this.props.location; - const { branches, component, loading } = this.state; + const { branchLikes, component, loading } = this.state; if (!loading && !component) { return <ComponentContainerNotFound />; } - 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 ( <div> {component && !['FIL', 'UTS'].includes(component.qualifier) && ( <ComponentNav - branches={branches} - currentBranch={branch} + branchLikes={branchLikes} component={component} + currentBranchLike={branchLike} currentTask={this.state.currentTask} isInProgress={this.state.isInProgress} isPending={this.state.isPending} @@ -186,8 +194,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { </div> ) : ( 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 = () => <div />; beforeEach(() => { - (getBranches as jest.Mock<any>).mockClear(); - (getComponentData as jest.Mock<any>).mockClear(); - (getComponentNavigation as jest.Mock<any>).mockClear(); - (getTasksForComponent as jest.Mock<any>).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<Props> { height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> <ComponentNavHeader - branches={this.props.branches} + branchLikes={this.props.branchLikes} component={this.props.component} - currentBranch={this.props.currentBranch} + currentBranchLike={this.props.currentBranchLike} // to close dropdown on any location change location={this.props.location} /> - <ComponentNavMeta branch={this.props.currentBranch} component={this.props.component} /> + <ComponentNavMeta + branchLike={this.props.currentBranchLike} + component={this.props.component} + /> <ComponentNavMenu - branch={this.props.currentBranch} + branchLike={this.props.currentBranchLike} component={this.props.component} // to re-render selected menu item location={this.props.location} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 8c600143589..d51dadbd73d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -20,22 +20,28 @@ import * as React from 'react'; import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; import SingleBranchHelperPopup from './SingleBranchHelperPopup'; import NoBranchSupportPopup from './NoBranchSupportPopup'; -import { Branch, Component } from '../../../types'; +import { BranchLike, Component } from '../../../types'; import * as theme from '../../../theme'; import BranchIcon from '../../../../components/icons-components/BranchIcon'; -import { isShortLivingBranch } from '../../../../helpers/branches'; +import { + isShortLivingBranch, + isSameBranchLike, + getBranchLikeDisplayName, + isPullRequest +} from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import HelpIcon from '../../../../components/icons-components/HelpIcon'; import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { - branches: Branch[]; + branchLikes: BranchLike[]; component: Component; - currentBranch: Branch; + currentBranchLike: BranchLike; location?: any; } @@ -69,7 +75,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State componentWillReceiveProps(nextProps: Props) { if ( nextProps.component !== this.props.component || - this.differentBranches(nextProps.currentBranch, this.props.currentBranch) || + !isSameBranchLike(nextProps.currentBranchLike, this.props.currentBranchLike) || nextProps.location !== this.props.location ) { this.setState({ dropdownOpen: false, singleBranchPopupOpen: false }); @@ -80,11 +86,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State this.mounted = false; } - differentBranches(a: Branch, b: Branch) { - // if main branch changes name, we should not close the dropdown - return a.isMain && b.isMain ? false : a.name !== b.name; - } - handleClick = (event: React.SyntheticEvent<HTMLElement>) => { event.preventDefault(); event.stopPropagation(); @@ -130,32 +131,46 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State const { configuration } = this.props.component; return this.state.dropdownOpen ? ( <ComponentNavBranchesMenu - branches={this.props.branches} + branchLikes={this.props.branchLikes} canAdmin={configuration && configuration.showSettings} component={this.props.component} - currentBranch={this.props.currentBranch} + currentBranchLike={this.props.currentBranchLike} onClose={this.closeDropdown} /> ) : null; }; renderMergeBranch = () => { - const { currentBranch } = this.props; - if (!isShortLivingBranch(currentBranch)) { + const { currentBranchLike } = this.props; + if (isShortLivingBranch(currentBranchLike)) { + return currentBranchLike.isOrphan ? ( + <span className="note big-spacer-left text-lowercase"> + {translate('branches.orphan_branch')} + <Tooltip overlay={translate('branches.orphan_branches.tooltip')}> + <i className="icon-help spacer-left" /> + </Tooltip> + </span> + ) : ( + <span className="note big-spacer-left text-lowercase"> + {translate('from')} <strong>{currentBranchLike.mergeBranch}</strong> + </span> + ); + } else if (isPullRequest(currentBranchLike)) { + return ( + <span className="note big-spacer-left text-lowercase"> + <FormattedMessage + defaultMessage={translate('branches.pull_request.for_merge_into_x_from_y')} + id="branches.pull_request.for_merge_into_x_from_y" + values={{ + base: <strong>{currentBranchLike.base}</strong>, + branch: <strong>{currentBranchLike.branch}</strong> + }} + /> + </span> + ); + } else { return null; } - return currentBranch.isOrphan ? ( - <span className="note big-spacer-left text-lowercase"> - {translate('branches.orphan_branch')} - <Tooltip overlay={translate('branches.orphan_branches.tooltip')}> - <i className="icon-help spacer-left" /> - </Tooltip> - </span> - ) : ( - <span className="note big-spacer-left text-lowercase"> - {translate('from')} <strong>{currentBranch.mergeBranch}</strong> - </span> - ); }; renderSingleBranchPopup = () => ( @@ -187,27 +202,33 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State ); render() { - const { branches, currentBranch } = this.props; + const { branchLikes, currentBranchLike } = this.props; if (this.context.onSonarCloud && !this.context.branchesEnabled) { return null; } + const displayName = getBranchLikeDisplayName(currentBranchLike); + if (!this.context.branchesEnabled) { return ( <div className="navbar-context-branches"> - <BranchIcon branch={currentBranch} className="little-spacer-right" fill={theme.gray80} /> - <span className="note">{currentBranch.name}</span> + <BranchIcon + branchLike={currentBranchLike} + className="little-spacer-right" + fill={theme.gray80} + /> + <span className="note">{displayName}</span> {this.renderNoBranchSupportPopup()} </div> ); } - if (branches.length < 2) { + if (branchLikes.length < 2) { return ( <div className="navbar-context-branches"> - <BranchIcon branch={currentBranch} className="little-spacer-right" /> - <span className="note">{currentBranch.name}</span> + <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> + <span className="note">{displayName}</span> {this.renderSingleBranchPopup()} </div> ); @@ -219,9 +240,9 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State open: this.state.dropdownOpen })}> <a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}> - <BranchIcon branch={currentBranch} className="little-spacer-right" /> - <Tooltip overlay={currentBranch.name} mouseEnterDelay={1}> - <span className="text-limited text-top">{currentBranch.name}</span> + <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> + <Tooltip overlay={displayName} mouseEnterDelay={1}> + <span className="text-limited text-top">{displayName}</span> </Tooltip> <i className="icon-dropdown little-spacer-left" /> </a> 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<Props, State> { @@ -54,7 +58,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, constructor(props: Props) { super(props); - this.state = { query: '', selected: null }; + this.state = { query: '', selected: undefined }; } componentDidMount() { @@ -65,10 +69,16 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, window.removeEventListener('click', this.handleClickOutside); } - getFilteredBranches = () => - 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<Props, } }; - handleSearchChange = (query: string) => this.setState({ query, selected: null }); + handleSearchChange = (query: string) => this.setState({ query, selected: undefined }); handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { switch (event.keyCode) { @@ -99,32 +109,31 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, openSelected = () => { 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<Props, return this.state.selected; } - const branches = this.getFilteredBranches(); - if (branches.find(b => 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 = () => ( <div className="menu-search"> @@ -161,21 +173,26 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, ); renderBranchesList = () => { - const branches = this.getFilteredBranches(); + const branchLikes = this.getFilteredBranchLikes(); const selected = this.getSelected(); - if (branches.length === 0) { + if (branchLikes.length === 0) { return <div className="menu-message note">{translate('no_results')}</div>; } - 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 ( - <React.Fragment key={branch.name}> + <React.Fragment key={getBranchLikeKey(branchLike)}> {showDivider && <li className="divider" />} {showOrphanHeader && ( <li className="dropdown-header"> @@ -185,12 +202,22 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, </Tooltip> </li> )} + {showPullRequestHeader && ( + <li className="dropdown-header navbar-context-meta-branch-menu-title"> + {translate('branches.pull_requests')} + </li> + )} + {showShortLivingBranchHeader && ( + <li className="dropdown-header navbar-context-meta-branch-menu-title"> + {translate('branches.short_lived_branches')} + </li> + )} <ComponentNavBranchesMenuItem - branch={branch} + branchLike={branchLike} component={this.props.component} - key={branch.name} + key={getBranchLikeKey(branchLike)} onSelect={this.handleSelect} - selected={branch.name === selected} + selected={isSameBranchLike(branchLike, selected)} /> </React.Fragment> ); 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 ( - <li key={branch.name} onMouseEnter={handleMouseEnter}> - <Tooltip mouseEnterDelay={0.5} overlay={branch.name} placement="right"> + <li key={getBranchLikeKey(branchLike)} onMouseEnter={handleMouseEnter}> + <Tooltip mouseEnterDelay={0.5} overlay={displayName} placement="right"> <Link className={classNames('navbar-context-meta-branch-menu-item', { active: props.selected })} - to={getProjectBranchUrl(props.component.key, branch)}> + to={getBranchLikeUrl(props.component.key, branchLike)}> <div className="navbar-context-meta-branch-menu-item-name text-ellipsis"> <BranchIcon - branch={branch} - className={classNames('little-spacer-right', { - 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan - })} + branchLike={branchLike} + className={classNames('little-spacer-right', { 'big-spacer-left': shouldBeIndented })} /> - {branch.name} - {branch.isMain && ( + {displayName} + {isMainBranch(branchLike) && ( <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div> )} </div> <div className="big-spacer-left note"> - <BranchStatus branch={branch} concise={true} /> + <BranchStatus branchLike={branchLike} concise={true} /> </div> </Link> </Tooltip> 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' && ( <PrivateBadge className="spacer-left" qualifier={component.qualifier} /> )} - {props.currentBranch && ( + {props.currentBranchLike && ( <ComponentNavBranch - branches={props.branches} + branchLikes={props.branchLikes} component={component} - currentBranch={props.currentBranch} + currentBranchLike={props.currentBranchLike} // to close dropdown on any location change location={props.location} /> 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<Props> { 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 ( <li> - <Link - to={{ - pathname, - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key - } - }} - activeClassName="active"> + <Link activeClassName="active" to={{ pathname, query: this.getQuery() }}> {translate('overview.page')} </Link> </li> @@ -108,15 +107,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { return ( <li> - <Link - to={{ - pathname: '/code', - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key - } - }} - activeClassName="active"> + <Link to={{ pathname: '/code', query: this.getQuery() }} activeClassName="active"> {this.isPortfolio() || this.isApplication() ? translate('view_projects.page') : translate('code.page')} @@ -126,20 +117,16 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } renderActivityLink() { - if (isShortLivingBranch(this.props.branch)) { + const { branchLike } = this.props; + + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { return null; } return ( <li> <Link - to={{ - pathname: '/project/activity', - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key - } - }} + to={{ pathname: '/project/activity', query: this.getQuery() }} activeClassName="active"> {translate('project_activity.page')} </Link> @@ -153,16 +140,9 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { return ( <li> <Link - to={{ - pathname: '/project/issues', - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key, - resolved: 'false' - } - }} + activeClassName="active" className={classNames({ active: isIssuesActive })} - activeClassName="active"> + to={{ pathname: '/project/issues', query: { ...this.getQuery(), resolved: 'false' } }}> {translate('issues.page')} </Link> </li> @@ -170,20 +150,16 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } renderComponentMeasuresLink() { - if (isShortLivingBranch(this.props.branch)) { + const { branchLike } = this.props; + + if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) { return null; } return ( <li> <Link - to={{ - pathname: '/component_measures', - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key - } - }} + to={{ pathname: '/component_measures', query: this.getQuery() }} activeClassName="active"> {translate('layout.measures')} </Link> @@ -192,30 +168,14 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { } 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 ( - <li> - <Link - className={classNames({ active: isSettingsActive })} - id="component-navigation-admin" - to={{ - pathname: '/project/settings', - query: { branch: getBranchName(branch), id: this.props.component.key } - }}> - {translate('branches.branch_settings')} - </Link> - </li> - ); - } - const adminLinks = this.renderAdministrationLinks(); if (!adminLinks.some(link => link != null)) { return null; @@ -260,13 +220,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { return ( <li key="settings"> <Link - to={{ - pathname: '/project/settings', - query: { - branch: getBranchName(this.props.branch), - id: this.props.component.key - } - }} + to={{ pathname: '/project/settings', query: this.getQuery() }} activeClassName="active"> {translate('project_settings.page')} </Link> @@ -448,7 +402,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { }; 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<Props> { 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 ( <div className="navbar-context-meta"> @@ -67,14 +63,13 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) { <DateTimeFormatter date={component.analysisDate} /> </div> )} - {component.version && - !shortBranch && ( - <Tooltip mouseEnterDelay={0.5} overlay={`${translate('version')} ${component.version}`}> - <div className="spacer-left text-limited"> - {translate('version')} {component.version} - </div> - </Tooltip> - )} + {displayVersion && ( + <Tooltip mouseEnterDelay={0.5} overlay={`${translate('version')} ${component.version}`}> + <div className="spacer-left text-limited"> + {translate('version')} {component.version} + </div> + </Tooltip> + )} {isLoggedIn(currentUser) && ( <div className="navbar-context-meta-secondary"> {mainBranch && ( @@ -90,15 +85,36 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) { )} </div> )} - {shortBranch && ( + {(isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && ( <div className="navbar-context-meta-secondary"> - <BranchStatus branch={branch!} /> + {isPullRequest(branchLike) && + branchLike.url !== undefined && ( + <a className="big-spacer-right" href={branchLike.url} rel="nofollow" target="_blank"> + {translate('branches.see_the_pr')} + <i className="icon-detach little-spacer-left" /> + </a> + )} + <BranchStatus branchLike={branchLike} /> </div> )} </div> ); } +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(<ComponentNav branches={[]} component={component} location={{}} />); + const wrapper = shallow( + <ComponentNav + branchLikes={[]} + component={component} + currentBranchLike={undefined} + location={{}} + /> + ); 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( <ComponentNavBranch - branches={[branch, fooBranch]} + branchLikes={[mainBranch, fooBranch]} component={component} - currentBranch={branch} + currentBranchLike={mainBranch} />, { context: { branchesEnabled: true } } ) @@ -58,9 +59,30 @@ it('renders short-living branch', () => { expect( shallow( <ComponentNavBranch - branches={[branch, fooBranch]} + branchLikes={[branch, fooBranch]} component={component} - currentBranch={branch} + currentBranchLike={branch} + />, + { 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( + <ComponentNavBranch + branchLikes={[pullRequest, fooBranch]} + component={component} + currentBranchLike={pullRequest} />, { 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( <ComponentNavBranch - branches={[branch, fooBranch]} + branchLikes={[mainBranch, fooBranch]} component={component} - currentBranch={branch} + currentBranchLike={mainBranch} />, { 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( - <ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />, + <ComponentNavBranch + branchLikes={[mainBranch]} + component={component} + currentBranchLike={mainBranch} + />, { 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( <ComponentNavBranch - branches={[branch, fooBranch]} + branchLikes={[mainBranch, fooBranch]} component={component} - currentBranch={branch} + currentBranchLike={mainBranch} />, { 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( - <ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />, + <ComponentNavBranch + branchLikes={[mainBranch]} + component={component} + currentBranchLike={mainBranch} + />, { 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( <ComponentNavBranchesMenu - branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]} + branchLikes={[ + mainBranch(), + shortBranch('foo'), + longBranch('bar'), + shortBranch('baz', true), + pullRequest('qux') + ]} component={component} - currentBranch={mainBranch()} + currentBranchLike={mainBranch()} onClose={jest.fn()} /> ) @@ -47,9 +54,9 @@ it('renders list', () => { it('searches', () => { const wrapper = shallow( <ComponentNavBranchesMenu - branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} + branchLikes={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} component={component} - currentBranch={mainBranch()} + currentBranchLike={mainBranch()} onClose={jest.fn()} /> ); @@ -60,21 +67,21 @@ it('searches', () => { it('selects next & previous', () => { const wrapper = shallow( <ComponentNavBranchesMenu - branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} + branchLikes={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} component={component} - currentBranch={mainBranch()} + currentBranchLike={mainBranch()} onClose={jest.fn()} /> ); 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( <ComponentNavBranchesMenuItem - branch={shortBranch} + branchLike={shortBranch} component={component} onSelect={jest.fn()} selected={false} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx index 912973b4558..676a38a577b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx @@ -32,7 +32,12 @@ it('should not render breadcrumbs with one element', () => { visibility: 'public' }; const result = shallow( - <ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} /> + <ComponentNavHeader + branchLikes={[]} + component={component} + currentBranchLike={undefined} + shouldOrganizationBeDisplayed={false} + /> ); expect(result).toMatchSnapshot(); }); @@ -53,8 +58,9 @@ it('should render organization', () => { }; const result = shallow( <ComponentNavHeader - branches={[]} + branchLikes={[]} component={component} + currentBranchLike={undefined} organization={organization} shouldOrganizationBeDisplayed={true} /> @@ -72,7 +78,12 @@ it('renders private badge', () => { visibility: 'private' }; const result = shallow( - <ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} /> + <ComponentNavHeader + branchLikes={[]} + component={component} + currentBranchLike={undefined} + shouldOrganizationBeDisplayed={false} + /> ); 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(<ComponentNavMenu branch={mainBranch} component={component} />, { + shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -58,7 +58,7 @@ it('should work with multiple extensions', () => { ] }; expect( - shallow(<ComponentNavMenu branch={mainBranch} component={component} />, { + shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -77,7 +77,7 @@ it('should work for short-living branches', () => { extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }; expect( - shallow(<ComponentNavMenu branch={branch} component={component} />, { + shallow(<ComponentNavMenu branchLike={branch} component={component} />, { context: { branchesEnabled: true } }) ).toMatchSnapshot(); @@ -89,7 +89,7 @@ it('should work for long-living branches', () => { expect( shallow( <ComponentNavMenu - branch={branch} + branchLike={branch} component={{ ...baseComponent, configuration: { showSettings }, @@ -109,7 +109,7 @@ it('should work for all qualifiers', () => { function checkWithQualifier(qualifier: string) { const component = { ...baseComponent, configuration: { showSettings: true }, qualifier }; expect( - shallow(<ComponentNavMenu branch={mainBranch} component={component} />, { + shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { 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( - <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} /> + <ComponentNavMeta + branchLike={branch} + component={component} + currentUser={{ isLoggedIn: false }} + /> ) ).toMatchSnapshot(); }); @@ -56,7 +60,31 @@ it('renders meta for long-living branch', () => { }; expect( shallow( - <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} /> + <ComponentNavMeta + branchLike={branch} + component={component} + currentUser={{ isLoggedIn: false }} + /> + ) + ).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( + <ComponentNavMeta + branchLike={pullRequest} + component={component} + currentUser={{ isLoggedIn: false }} + /> ) ).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" > <Connect(ComponentNavHeader) - branches={Array []} + branchLikes={Array []} component={ Object { "breadcrumbs": Array [ diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index 3571893cc96..6e45772bb56 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -10,7 +10,7 @@ exports[`renders main branch 1`] = ` onClick={[Function]} > <BranchIcon - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -41,7 +41,7 @@ exports[`renders no branch support popup 1`] = ` className="navbar-context-branches" > <BranchIcon - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -77,6 +77,63 @@ exports[`renders no branch support popup 1`] = ` </div> `; +exports[`renders pull request 1`] = ` +<div + className="navbar-context-branches dropdown" +> + <a + className="link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <BranchIcon + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + "url": "https://example.com/pull/1234", + } + } + className="little-spacer-right" + /> + <Tooltip + mouseEnterDelay={1} + overlay="1234 – Feature PR" + placement="bottom" + > + <span + className="text-limited text-top" + > + 1234 – Feature PR + </span> + </Tooltip> + <i + className="icon-dropdown little-spacer-left" + /> + </a> + <span + className="note big-spacer-left text-lowercase" + > + <FormattedMessage + defaultMessage="branches.pull_request.for_merge_into_x_from_y" + id="branches.pull_request.for_merge_into_x_from_y" + values={ + Object { + "base": <strong> + master + </strong>, + "branch": <strong> + feature + </strong>, + } + } + /> + </span> +</div> +`; + exports[`renders short-living branch 1`] = ` <div className="navbar-context-branches dropdown" @@ -87,7 +144,7 @@ exports[`renders short-living branch 1`] = ` onClick={[Function]} > <BranchIcon - branch={ + branchLike={ Object { "isMain": false, "mergeBranch": "master", @@ -134,7 +191,7 @@ exports[`renders single branch popup 1`] = ` className="navbar-context-branches" > <BranchIcon - branch={ + branchLike={ Object { "isMain": true, "name": "master", diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index 6946e02b328..cc51efd1976 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -19,10 +19,10 @@ exports[`renders list 1`] = ` className="menu menu-vertically-limited" > <React.Fragment - key="master" + key="branch-master" > <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -33,13 +33,45 @@ exports[`renders list 1`] = ` "key": "component", } } - key="master" + key="branch-master" onSelect={[Function]} selected={true} /> </React.Fragment> <React.Fragment - key="baz" + key="pull-request-1234" + > + <li + className="dropdown-header navbar-context-meta-branch-menu-title" + > + branches.pull_requests + </li> + <ComponentNavBranchesMenuItem + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "status": Object { + "bugs": 0, + "codeSmells": 0, + "vulnerabilities": 0, + }, + "title": "qux", + } + } + component={ + Object { + "key": "component", + } + } + key="pull-request-1234" + onSelect={[Function]} + selected={false} + /> + </React.Fragment> + <React.Fragment + key="branch-baz" > <li className="divider" @@ -58,7 +90,7 @@ exports[`renders list 1`] = ` </Tooltip> </li> <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "isOrphan": true, @@ -77,16 +109,16 @@ exports[`renders list 1`] = ` "key": "component", } } - key="baz" + key="branch-baz" onSelect={[Function]} selected={false} /> </React.Fragment> <React.Fragment - key="foo" + key="branch-foo" > <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "isOrphan": undefined, @@ -105,19 +137,19 @@ exports[`renders list 1`] = ` "key": "component", } } - key="foo" + key="branch-foo" onSelect={[Function]} selected={false} /> </React.Fragment> <React.Fragment - key="bar" + key="branch-bar" > <li className="divider" /> <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "name": "bar", @@ -129,13 +161,13 @@ exports[`renders list 1`] = ` "key": "component", } } - key="bar" + key="branch-bar" onSelect={[Function]} selected={false} /> </React.Fragment> <React.Fragment - key="baz" + key="branch-baz" > <li className="divider" @@ -154,7 +186,7 @@ exports[`renders list 1`] = ` </Tooltip> </li> <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "isOrphan": true, @@ -173,7 +205,7 @@ exports[`renders list 1`] = ` "key": "component", } } - key="baz" + key="branch-baz" onSelect={[Function]} selected={false} /> @@ -201,10 +233,15 @@ exports[`searches 1`] = ` className="menu menu-vertically-limited" > <React.Fragment - key="foobar" + key="branch-foobar" > + <li + className="dropdown-header navbar-context-meta-branch-menu-title" + > + branches.short_lived_branches + </li> <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "isOrphan": undefined, @@ -223,19 +260,19 @@ exports[`searches 1`] = ` "key": "component", } } - key="foobar" + key="branch-foobar" onSelect={[Function]} selected={true} /> </React.Fragment> <React.Fragment - key="bar" + key="branch-bar" > <li className="divider" /> <ComponentNavBranchesMenuItem - branch={ + branchLike={ Object { "isMain": false, "name": "bar", @@ -247,7 +284,7 @@ exports[`searches 1`] = ` "key": "component", } } - key="bar" + key="branch-bar" onSelect={[Function]} selected={false} /> 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`] = ` <li - key="master" + key="branch-master" onMouseEnter={[Function]} > <Tooltip @@ -27,7 +27,7 @@ exports[`renders main branch 1`] = ` className="navbar-context-meta-branch-menu-item-name text-ellipsis" > <BranchIcon - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -46,7 +46,7 @@ exports[`renders main branch 1`] = ` className="big-spacer-left note" > <BranchStatus - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -62,7 +62,7 @@ exports[`renders main branch 1`] = ` exports[`renders short-living branch 1`] = ` <li - key="foo" + key="branch-foo" onMouseEnter={[Function]} > <Tooltip @@ -89,7 +89,7 @@ exports[`renders short-living branch 1`] = ` className="navbar-context-meta-branch-menu-item-name text-ellipsis" > <BranchIcon - branch={ + branchLike={ Object { "isMain": false, "mergeBranch": "master", @@ -110,7 +110,7 @@ exports[`renders short-living branch 1`] = ` className="big-spacer-left note" > <BranchStatus - branch={ + branchLike={ Object { "isMain": false, "mergeBranch": "master", @@ -133,7 +133,7 @@ exports[`renders short-living branch 1`] = ` exports[`renders short-living orhpan branch 1`] = ` <li - key="foo" + key="branch-foo" onMouseEnter={[Function]} > <Tooltip @@ -160,7 +160,7 @@ exports[`renders short-living orhpan branch 1`] = ` className="navbar-context-meta-branch-menu-item-name text-ellipsis" > <BranchIcon - branch={ + branchLike={ Object { "isMain": false, "isOrphan": true, @@ -182,7 +182,7 @@ exports[`renders short-living orhpan branch 1`] = ` className="big-spacer-left note" > <BranchStatus - branch={ + branchLike={ Object { "isMain": false, "isOrphan": true, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap index eb990ac6401..dd13db8a75c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap @@ -23,7 +23,6 @@ exports[`should not render breadcrumbs with one element 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "my-project", }, } @@ -91,7 +90,6 @@ exports[`should render organization 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "my-project", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 22c56c67930..5665e5ab9b4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -11,7 +11,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -30,7 +29,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -49,7 +47,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -67,7 +64,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -85,7 +81,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -123,7 +118,6 @@ exports[`should work for all qualifiers 1`] = ` Object { "pathname": "/project/settings", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -205,7 +199,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -224,7 +217,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -243,7 +235,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -261,7 +252,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -279,7 +269,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -317,7 +306,6 @@ exports[`should work for all qualifiers 2`] = ` Object { "pathname": "/project/settings", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -342,7 +330,6 @@ exports[`should work for all qualifiers 3`] = ` Object { "pathname": "/portfolio", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -361,7 +348,6 @@ exports[`should work for all qualifiers 3`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -380,7 +366,6 @@ exports[`should work for all qualifiers 3`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -398,7 +383,6 @@ exports[`should work for all qualifiers 3`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -416,7 +400,6 @@ exports[`should work for all qualifiers 3`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -478,7 +461,6 @@ exports[`should work for all qualifiers 4`] = ` Object { "pathname": "/portfolio", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -497,7 +479,6 @@ exports[`should work for all qualifiers 4`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -516,7 +497,6 @@ exports[`should work for all qualifiers 4`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -534,7 +514,6 @@ exports[`should work for all qualifiers 4`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -552,7 +531,6 @@ exports[`should work for all qualifiers 4`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -575,7 +553,6 @@ exports[`should work for all qualifiers 5`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -594,7 +571,6 @@ exports[`should work for all qualifiers 5`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -613,7 +589,6 @@ exports[`should work for all qualifiers 5`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -631,7 +606,6 @@ exports[`should work for all qualifiers 5`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -649,7 +623,6 @@ exports[`should work for all qualifiers 5`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -948,7 +921,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -967,7 +939,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -986,7 +957,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1004,7 +974,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1022,7 +991,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1060,7 +1028,6 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/project/settings", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1200,7 +1167,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1219,7 +1185,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "foo", "resolved": "false", }, @@ -1238,7 +1203,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1256,7 +1220,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/code", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1274,7 +1237,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -1312,7 +1274,6 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/project/settings", "query": Object { - "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap index 437ab7a3477..46e1c4becd7 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap @@ -27,6 +27,51 @@ exports[`renders meta for long-living branch 1`] = ` </div> `; +exports[`renders meta for pull request 1`] = ` +<div + className="navbar-context-meta" +> + <div + className="spacer-left" + > + <DateTimeFormatter + date="2017-01-02T00:00:00.000Z" + /> + </div> + <div + className="navbar-context-meta-secondary" + > + <a + className="big-spacer-right" + href="https://example.com/pull/1234" + rel="nofollow" + target="_blank" + > + branches.see_the_pr + <i + className="icon-detach little-spacer-left" + /> + </a> + <BranchStatus + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "status": Object { + "bugs": 0, + "codeSmells": 2, + "vulnerabilities": 3, + }, + "title": "Feature PR", + "url": "https://example.com/pull/1234", + } + } + /> + </div> +</div> +`; + exports[`renders status of short-living branch 1`] = ` <div className="navbar-context-meta" @@ -42,7 +87,7 @@ exports[`renders status of short-living branch 1`] = ` className="navbar-context-meta-secondary" > <BranchStatus - branch={ + branchLike={ Object { "isMain": false, "mergeBranch": "master", diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap index 0a9d6781bf0..4ad1c1e2e2d 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap @@ -21,7 +21,6 @@ exports[`renders favorite 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -70,7 +69,6 @@ exports[`renders match 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -118,7 +116,6 @@ exports[`renders organizations 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -171,7 +168,6 @@ exports[`renders organizations 2`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -219,7 +215,6 @@ exports[`renders projects 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "qwe", }, } @@ -272,7 +267,6 @@ exports[`renders recently browsed 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -320,7 +314,6 @@ exports[`renders selected 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -366,7 +359,6 @@ exports[`renders selected 2`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 92136630ff4..106d76e679f 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -36,7 +36,15 @@ export interface AppState { qualifiers: string[]; } -export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch; +export interface Branch { + analysisDate?: string; + isMain: boolean; + name: string; +} + +export type BranchLike = Branch | PullRequest; + +export type BranchParameters = { branch?: string } | { pullRequest?: string }; export enum BranchType { LONG = 'LONG', @@ -273,23 +281,14 @@ export interface LoggedInUser extends CurrentUser { name: string; } -export interface LongLivingBranch { - analysisDate?: string; +export interface LongLivingBranch extends Branch { isMain: false; - name: string; - status?: { - qualityGateStatus: string; - }; + status?: { qualityGateStatus: string }; type: BranchType.LONG; } -export interface MainBranch { - analysisDate?: string; +export interface MainBranch extends Branch { isMain: true; - name: string; - status?: { - qualityGateStatus: string; - }; } export interface Metric { @@ -352,6 +351,21 @@ export interface ProjectLink { url: string; } +export interface PullRequest { + analysisDate?: string; + base: string; + branch: string; + key: string; + isOrphan?: true; + status?: { + bugs: number; + codeSmells: number; + vulnerabilities: number; + }; + title: string; + url?: string; +} + export interface Rule { isTemplate?: boolean; key: string; @@ -419,12 +433,10 @@ export enum RuleScope { All = 'ALL' } -export interface ShortLivingBranch { - analysisDate?: string; +export interface ShortLivingBranch extends Branch { isMain: false; isOrphan?: true; mergeBranch: string; - name: string; status?: { bugs: number; codeSmells: number; diff --git a/server/sonar-web/src/main/js/apps/about/actions.js b/server/sonar-web/src/main/js/apps/about/actions.js index 2c63a8553b9..a60d730c30a 100644 --- a/server/sonar-web/src/main/js/apps/about/actions.js +++ b/server/sonar-web/src/main/js/apps/about/actions.js @@ -23,7 +23,7 @@ import { receiveValues } from '../settings/store/values/actions'; export const fetchAboutPageSettings = () => 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<any>) => { }; export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch<any>) => { - 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) { <td> {task.branchType === 'SHORT' && <ShortLivingBranchIcon className="little-spacer-right" />} {task.branchType === 'LONG' && <LongLivingBranchIcon className="little-spacer-right" />} + {task.pullRequest !== undefined && <PullRequestIcon className="little-spacer-right" />} {!task.branchType && + !task.pullRequest && task.componentQualifier && ( <span className="little-spacer-right"> <QualifierIcon qualifier={task.componentQualifier} /> @@ -56,7 +65,7 @@ export default function TaskComponent({ task }: Props) { {task.organization && <Organization organizationKey={task.organization} />} {task.componentName && ( - <Link className="spacer-right" to={getProjectUrl(task.componentKey, task.branch)}> + <Link className="spacer-right" to={getTaskComponentUrl(task.componentKey, task)}> {task.componentName} {task.branch && ( @@ -65,6 +74,15 @@ export default function TaskComponent({ task }: Props) { {task.branch} </span> )} + + {task.pullRequest && ( + <Tooltip overlay={task.pullRequestTitle}> + <span className="text-limited text-text-top"> + <span style={{ marginLeft: 5, marginRight: 5 }}>/</span> + {task.pullRequest} + </span> + </Tooltip> + )} </Link> )} @@ -72,3 +90,15 @@ export default function TaskComponent({ task }: Props) { </td> ); } + +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<Props, State> { } 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<Props, State> { } 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<Props, State> { 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<Props, State> { 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<Props, State> { }; 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<Props, State> { 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<Props, State> { {error && <div className="alert alert-danger">{error}</div>} <Search - branch={branchName} + branchLike={branchLike} component={component} location={location} onError={this.handleError} @@ -210,7 +211,11 @@ export default class App extends React.PureComponent<Props, State> { <div className="code-components"> {shouldShowBreadcrumbs && ( - <Breadcrumbs branch={branchName} breadcrumbs={breadcrumbs} rootComponent={component} /> + <Breadcrumbs + branchLike={branchLike} + breadcrumbs={breadcrumbs} + rootComponent={component} + /> )} {sourceViewer === undefined && @@ -218,7 +223,7 @@ export default class App extends React.PureComponent<Props, State> { <div className={componentsClassName}> <Components baseComponent={baseComponent} - branch={branchName} + branchLike={branchLike} components={components} rootComponent={component} /> @@ -232,7 +237,7 @@ export default class App extends React.PureComponent<Props, State> { {sourceViewer !== undefined && ( <div className="spacer-top"> - <SourceViewer branch={branchName} component={sourceViewer.key} /> + <SourceViewer branchLike={branchLike} component={sourceViewer.key} /> </div> )} </div> 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 ( <ul className="code-breadcrumbs"> {breadcrumbs.map((component, index) => ( <li key={component.key}> <ComponentName - branch={branch} + branchLike={branchLike} canBrowse={index < breadcrumbs.length - 1} component={component} rootComponent={rootComponent} diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx index 841d96baee7..df873439506 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -24,12 +24,13 @@ import ComponentMeasure from './ComponentMeasure'; import ComponentLink from './ComponentLink'; import ComponentPin from './ComponentPin'; import { Component as IComponent } from '../types'; +import { BranchLike } from '../../../app/types'; const TOP_OFFSET = 200; const BOTTOM_OFFSET = 10; interface Props { - branch?: string; + branchLike?: BranchLike; canBrowse?: boolean; component: IComponent; previous?: IComponent; @@ -73,7 +74,7 @@ export default class Component extends React.PureComponent<Props> { render() { const { - branch, + branchLike, component, rootComponent, selected = false, @@ -89,10 +90,10 @@ export default class Component extends React.PureComponent<Props> { switch (component.qualifier) { case 'FIL': case 'UTS': - componentAction = <ComponentPin branch={branch} component={component} />; + componentAction = <ComponentPin branchLike={branchLike} component={component} />; break; default: - componentAction = <ComponentLink branch={branch} component={component} />; + componentAction = <ComponentLink branchLike={branchLike} component={component} />; } } @@ -121,7 +122,7 @@ export default class Component extends React.PureComponent<Props> { </td> <td className="code-name-cell"> <ComponentName - branch={branch} + branchLike={branchLike} component={component} rootComponent={rootComponent} previous={previous} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx index 599dabdc412..60927947472 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx @@ -20,21 +20,22 @@ import * as React from 'react'; import { Link } from 'react-router'; import { Component } from '../types'; +import { BranchLike } from '../../../app/types'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import { translate } from '../../../helpers/l10n'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../helpers/urls'; interface Props { - branch?: string; + branchLike?: BranchLike; component: Component; } -export default function ComponentLink({ component, branch }: Props) { +export default function ComponentLink({ component, branchLike }: Props) { return ( <Link className="link-no-underline" title={translate('code.open_component_page')} - to={getProjectUrl(component.refKey || component.key, branch)}> + to={getBranchLikeUrl(component.refKey || component.key, branchLike)}> <LinkIcon /> </Link> ); 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) { </Link> ); } 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<HTMLAnchorElement>) => { 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 ( <table className="data zebra"> <ComponentsHeader baseComponent={baseComponent} rootComponent={rootComponent} /> {baseComponent && ( <tbody> <Component - branch={branch} + branchLike={branchLike} component={baseComponent} key={baseComponent.key} rootComponent={rootComponent} @@ -53,7 +54,7 @@ export default function Components(props: Props) { {components.length ? ( components.map((component, index, list) => ( <Component - branch={branch} + branchLike={branchLike} canBrowse={true} component={component} key={component.key} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 4a5536ed3de..abeeacc511a 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -21,15 +21,17 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import * as classNames from 'classnames'; import Components from './Components'; -import { getTree } from '../../../api/components'; -import { parseError } from '../../../helpers/request'; -import { getProjectUrl } from '../../../helpers/urls'; import { Component } from '../types'; +import { getTree } from '../../../api/components'; +import { BranchLike } from '../../../app/types'; import SearchBox from '../../../components/controls/SearchBox'; +import { getBranchLikeQuery } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; +import { parseError } from '../../../helpers/request'; +import { getProjectUrl } from '../../../helpers/urls'; interface Props { - branch?: string; + branchLike?: BranchLike; component: Component; location: {}; onError: (error: string) => void; @@ -89,7 +91,7 @@ export default class Search extends React.PureComponent<Props, State> { } 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<Props, State> { } 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<Props, State> { 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<Props, State> { {results != null && ( <div className="boxed-group boxed-group-inner spacer-top"> <Components - branch={this.props.branch} + branchLike={this.props.branchLike} components={results} rootComponent={component} selected={selected} diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts index 80fdbadea8c..75ef812c977 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.ts +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -28,6 +28,8 @@ import { } from './bucket'; import { Breadcrumb, Component } from './types'; import { getChildren, getComponent, getBreadcrumbs } from '../../api/components'; +import { BranchLike } from '../../app/types'; +import { getBranchLikeQuery } from '../../helpers/branches'; const METRICS = [ 'ncloc', @@ -54,11 +56,15 @@ function requestChildren( componentKey: string, metrics: string[], page: number, - branch?: string + branchLike?: BranchLike ): Promise<Component[]> { - 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<Component[]> { - return requestChildren(componentKey, metrics, 1, branch); + return requestChildren(componentKey, metrics, 1, branchLike); } interface Children { @@ -84,13 +90,13 @@ interface ExpandRootDirFunc { (children: Children): Promise<Children>; } -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<Breadcrumb[]> { - 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<Children> { 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<string>, - branch?: string + branchLike?: { id?: string; name: string } ) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, 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 <i className="spinner spinner-margin" />; } - 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 && ( <MeasureContentContainer - branch={getBranchName(branch)} + branchLike={branchLike} className="layout-page-main" currentUser={this.props.currentUser} rootComponent={component} @@ -191,7 +191,7 @@ export default class App extends React.PureComponent { {metric == null && hasBubbleChart(query.metric) && ( <MeasureOverviewContainer - branch={getBranchName(branch)} + branchLike={branchLike} className="layout-page-main" rootComponent={component} currentUser={this.props.currentUser} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js index a9122702894..54d63a4dddd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -27,6 +27,7 @@ import { fetchMetrics } from '../../../store/rootActions'; import { getMeasuresAndMeta } from '../../../api/measures'; import { getLeakPeriod } from '../../../helpers/periods'; import { enhanceMeasure } from '../../../components/measure/utils'; +import { getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component, Period } from '../types'; */ /*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */ @@ -50,7 +51,7 @@ function banQualityGate(component /*: Component */) /*: Array<Measure> */ { const fetchMeasures = ( component /*: string */, metricsKey /*: Array<string> */, - 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 ( <div className="measure-details-viewer"> <CodeView - branch={this.props.branch} + branchLike={this.props.branchLike} component={this.props.component} components={this.state.components} leakPeriod={this.props.leakPeriod} @@ -244,7 +245,7 @@ export default class MeasureContent extends React.PureComponent { const selectedIdx = this.getSelectedIndex(); return ( <FilesView - branch={this.props.branch} + branchLike={this.props.branchLike} components={this.state.components} fetchMore={this.fetchMoreComponents} handleOpen={this.onOpenComponent} @@ -261,7 +262,7 @@ export default class MeasureContent extends React.PureComponent { if (view === 'treemap') { return ( <TreeMapView - branch={this.props.branch} + branchLike={this.props.branchLike} components={this.state.components} handleSelect={this.onOpenComponent} metric={metric} @@ -274,7 +275,7 @@ export default class MeasureContent extends React.PureComponent { } render() { - const { branch, component, currentUser, measure, metric, rootComponent, view } = this.props; + const { branchLike, component, currentUser, measure, metric, rootComponent, view } = this.props; const isLoggedIn = currentUser && currentUser.isLoggedIn; const isFile = isFileType(component); const selectedIdx = this.getSelectedIndex(); @@ -288,7 +289,7 @@ export default class MeasureContent extends React.PureComponent { <div className="layout-page-main-inner"> <Breadcrumbs backToFirst={view === 'list'} - branch={branch} + branchLike={branchLike} className="measure-breadcrumbs spacer-right text-ellipsis" component={component} handleSelect={this.onOpenComponent} @@ -327,7 +328,7 @@ export default class MeasureContent extends React.PureComponent { measure != null && ( <div className="layout-page-main-inner measure-details-content"> <MeasureHeader - branch={branch} + branchLike={branchLike} component={component} components={this.state.components} leakPeriod={this.props.leakPeriod} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js index beaca1bd6e1..1998460996e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -26,14 +26,14 @@ import MeasureContent from './MeasureContent'; /*:: import type { RawQuery } from '../../../helpers/query'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, className?: string, currentUser: { isLoggedIn: boolean }, rootComponent: Component, fetchMeasures: ( component: string, metricsKey: Array<string>, - branch?: string + branchLike?: { id?: string; name: string } ) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, 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 ( <MeasureContent - branch={this.props.branch} + branchLike={this.props.branchLike} className={this.props.className} component={this.state.component} currentUser={this.props.currentUser} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureFavoriteContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureFavoriteContainer.js index 1690866ffde..f1aac5309b1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureFavoriteContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureFavoriteContainer.js @@ -57,7 +57,7 @@ class MeasureFavoriteContainer extends React.PureComponent { } fetchComponentFavorite({ component, onReceiveComponent } /*: Props */) { - getComponentForSourceViewer(component).then(component => { + 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<Component>, 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')}> <Link className="js-show-history spacer-left button button-small" - to={getMeasureHistoryUrl(component.key, metric.key, branch)}> + to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}> <HistoryIcon /> </Link> </Tooltip> 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 ( <div className="measure-details-viewer"> - <SourceViewer branch={branch} component={component.key} /> + <SourceViewer branchLike={branchLike} component={component.key} /> </div> ); } @@ -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 { <div className="layout-page-main-inner"> <Breadcrumbs backToFirst={true} - branch={branch} + branchLike={branchLike} className="measure-breadcrumbs spacer-right text-ellipsis" component={component} handleSelect={this.props.updateSelected} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js index 7268b08f308..b69a9a8378f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js @@ -23,12 +23,13 @@ import MeasureOverview from './MeasureOverview'; import { getComponentShow } from '../../../api/components'; import { getProjectUrl } from '../../../helpers/urls'; import { isViewType } from '../utils'; +import { getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component, Period, Query } from '../types'; */ /*:: import type { RawQuery } from '../../../helpers/query'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, className?: string, rootComponent: Component, currentUser: { isLoggedIn: boolean }, @@ -81,14 +82,14 @@ export default class MeasureOverviewContainer extends React.PureComponent { this.mounted = false; } - fetchComponent = ({ branch, rootComponent, selected } /*: Props */) => { + 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 ( <MeasureOverview - branch={this.props.branch} + branchLike={this.props.branchLike} className={this.props.className} component={this.state.component} currentUser={this.props.currentUser} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js index dfa9a644f24..5b02e3d87fb 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js @@ -77,7 +77,10 @@ it('should render correctly for leak', () => { }); it('should render with branch', () => { - expect(shallow(<MeasureHeader branch="feature" {...PROPS} />).find('Link')).toMatchSnapshot(); + const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' }; + expect( + shallow(<MeasureHeader branchLike={shortBranch} {...PROPS} />).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<ComponentEnhanced>, leakPeriod?: Period, @@ -81,7 +81,7 @@ export default class CodeView extends React.PureComponent { }; render() { - const { branch, component } = this.props; - return <SourceViewer branch={branch} component={component.key} />; + const { branchLike, component } = this.props; + return <SourceViewer branchLike={branchLike} component={component.key} />; } } 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 ( <td className="measure-details-component-cell"> <div className="text-ellipsis"> + {/* TODO make this <a> link a react-router <Link /> */} {component.refKey == null ? ( <a id={'component-measures-component-link-' + component.key} className="link-no-underline" - href={getPathUrlAsString(getProjectUrl(component.key, branch))} + href={getPathUrlAsString(getBranchLikeUrl(component.key, branchLike))} onClick={this.handleClick}> {this.renderInner()} </a> @@ -81,7 +82,7 @@ export default class ComponentCell extends React.PureComponent { <Link className="link-no-underline" id={'component-measures-component-link-' + component.key} - to={getProjectUrl(component.refKey, branch)}> + to={getBranchLikeUrl(component.refKey, branchLike)}> <span className="big-spacer-right"> <LinkIcon /> </span> 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<ComponentEnhanced>, 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 <EmptyResult />; @@ -65,7 +65,7 @@ export default function ComponentsList( {components.map(component => ( <ComponentsListRow key={component.id} - branch={branch} + branchLike={branchLike} component={component} otherMetrics={otherMetrics} isSelected={component.key === selectedComponent} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js index e87b224967f..47a5d2bf29a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js @@ -26,7 +26,7 @@ import MeasureCell from './MeasureCell'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, component: ComponentEnhanced, isSelected: boolean, onClick: string => 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 ( <tr className={rowClass}> - <ComponentCell branch={branch} component={component} onClick={props.onClick} /> + <ComponentCell branchLike={branchLike} component={component} onClick={props.onClick} /> <MeasureCell component={component} metric={props.metric} /> 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<ComponentEnhanced>, fetchMore: () => void, handleSelect: string => void, @@ -123,7 +123,7 @@ export default class ListView extends React.PureComponent { return ( <div ref={elem => (this.listContainer = elem)}> <ComponentsList - branch={this.props.branch} + branchLike={this.props.branchLike} components={this.props.components} metrics={this.props.metrics} metric={this.props.metric} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js index 3892abf2689..1dd3fd68f26 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js @@ -29,13 +29,13 @@ import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import TreeMap from '../../../components/charts/TreeMap'; import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; -import { getProjectUrl } from '../../../helpers/urls'; +import { getBranchLikeUrl } from '../../../helpers/urls'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: import type { ComponentEnhanced } from '../types'; */ /*:: import type { TreeMapItem } from '../../../components/charts/TreeMap'; */ /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, components: Array<ComponentEnhanced>, 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<Props> { }; 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 ( <div className="page page-limited"> <SourceViewer aroundLine={finalLine} - branch={branch} + branchLike={fakeBranchLike} component={id} highlightedLine={finalLine} onLoaded={this.scrollToLine} diff --git a/server/sonar-web/src/main/js/apps/component/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/component/components/__tests__/__snapshots__/App-test.tsx.snap index 1cb5c653533..f5dd7e1a6ce 100644 --- a/server/sonar-web/src/main/js/apps/component/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component/components/__tests__/__snapshots__/App-test.tsx.snap @@ -6,7 +6,14 @@ exports[`renders 1`] = ` > <Connect(SourceViewerBase) aroundLine={7} - branch="b" + branchLike={ + Object { + "isMain": false, + "mergeBranch": "", + "name": "b", + "type": "SHORT", + } + } component="foo" highlightedLine={7} onLoaded={[Function]} diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.d.ts b/server/sonar-web/src/main/js/apps/issues/components/App.d.ts index 9bafb845242..211061e58bd 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.d.ts +++ b/server/sonar-web/src/main/js/apps/issues/components/App.d.ts @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Component, CurrentUser } from '../../../app/types'; +import { Component, CurrentUser, BranchLike } from '../../../app/types'; import { RawQuery } from '../../../helpers/query'; interface Props { - branch?: { name: string }; + branchLike?: BranchLike; component?: Component; currentUser: CurrentUser; fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<any>; 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 { <div> {paging.total > 0 && ( <IssuesList - branch={getBranchName(branch)} + branchLike={branchLike} checked={this.state.checked} component={component} issues={issues} @@ -971,7 +976,7 @@ export default class App extends React.PureComponent { {openIssue != null ? ( <div className="pull-left width-60"> <ComponentBreadcrumbs - branch={getBranchName(this.props.branch)} + branchLike={this.props.branchLike} component={component} issue={openIssue} organization={this.props.organization} @@ -1000,7 +1005,7 @@ export default class App extends React.PureComponent { <div> {openIssue ? ( <IssuesSourceViewer - branch={getBranchName(this.props.branch)} + branchLike={this.props.branchLike} component={component} openIssue={openIssue} loadIssues={this.fetchIssuesForComponent} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index 9da37298c41..c86ec19b057 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -21,11 +21,11 @@ import * as React from 'react'; import { Link } from 'react-router'; import Organization from '../../../components/shared/Organization'; import { collapsePath, limitComponentName } from '../../../helpers/path'; -import { getProjectUrl } from '../../../helpers/urls'; -import { Component } from '../../../app/types'; +import { getBranchLikeUrl, getCodeUrl } from '../../../helpers/urls'; +import { Component, BranchLike } from '../../../app/types'; interface Props { - branch?: string; + branchLike?: BranchLike; component?: Component; issue: { component: string; @@ -39,7 +39,12 @@ interface Props { organization?: { key: string }; } -export default function ComponentBreadcrumbs({ branch, component, issue, organization }: Props) { +export default function ComponentBreadcrumbs({ + branchLike, + component, + issue, + organization +}: Props) { const displayOrganization = !organization && (component == null || ['VW', 'SVW'].includes(component.qualifier)); const displayProject = component == null || !['TRK', 'BRC', 'DIR'].includes(component.qualifier); @@ -53,7 +58,7 @@ export default function ComponentBreadcrumbs({ branch, component, issue, organiz {displayProject && ( <span title={issue.projectName}> - <Link to={getProjectUrl(issue.project, branch)} className="link-no-underline"> + <Link to={getBranchLikeUrl(issue.project, branchLike)} className="link-no-underline"> {limitComponentName(issue.projectName)} </Link> <span className="slash-separator" /> @@ -64,14 +69,16 @@ export default function ComponentBreadcrumbs({ branch, component, issue, organiz issue.subProject !== undefined && issue.subProjectName !== undefined && ( <span title={issue.subProjectName}> - <Link to={getProjectUrl(issue.subProject, branch)} className="link-no-underline"> + <Link to={getBranchLikeUrl(issue.subProject, branchLike)} className="link-no-underline"> {limitComponentName(issue.subProjectName)} </Link> <span className="slash-separator" /> </span> )} - <Link to={getProjectUrl(issue.component, branch)} className="link-no-underline"> + <Link + to={getCodeUrl(issue.project, branchLike, issue.component)} + className="link-no-underline"> <span title={issue.componentLongName}>{collapsePath(issue.componentLongName)}</span> </Link> </div> 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<string>, component?: Component, issues: Array<Issue>, @@ -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 ( <div> {issues.map((issue, index) => ( <ListItem - branch={branch} + branchLike={branchLike} checked={checked.includes(issue.key)} component={component} key={issue.key} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js index 6f7422c2290..fb72e2eca3b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -26,7 +26,7 @@ import { scrollToElement } from '../../../helpers/scrolling'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, component: Component, loadIssues: (string, number, number) => Promise<*>, onIssueChange: Issue => void, @@ -107,7 +107,7 @@ export default class IssuesSourceViewer extends React.PureComponent { <div ref={node => (this.node = node)}> <SourceViewer aroundLine={aroundLine} - branch={this.props.branch} + branchLike={this.props.branchLike} component={openIssue.component} displayAllIssues={true} displayIssueLocationsCount={false} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js index 28c6ae69760..e4802fc64a3 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js @@ -26,7 +26,7 @@ import Issue from '../../../components/issue/Issue'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, checked: boolean, component?: Component, issue: IssueType, @@ -89,7 +89,7 @@ export default class ListItem extends React.PureComponent { }; render() { - const { branch, component, issue, previousIssue } = this.props; + const { branchLike, component, issue, previousIssue } = this.props; const displayComponent = previousIssue == null || previousIssue.component !== issue.component; @@ -98,7 +98,7 @@ export default class ListItem extends React.PureComponent { {displayComponent && ( <div className="issues-workspace-list-component"> <ComponentBreadcrumbs - branch={branch} + branchLike={branchLike} component={component} issue={this.props.issue} organization={this.props.organization} @@ -106,7 +106,7 @@ export default class ListItem extends React.PureComponent { </div> )} <Issue - branch={branch} + branchLike={branchLike} checked={this.props.checked} displayLocationsLink={false} issue={issue} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx index 3bee431c81c..00c83d10dcd 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ComponentBreadcrumbs from '../ComponentBreadcrumbs'; +import { ShortLivingBranch, BranchType } from '../../../../app/types'; const baseIssue = { component: 'comp', @@ -40,5 +41,13 @@ it('renders with sub-project', () => { it('renders with branch', () => { const issue = { ...baseIssue, subProject: 'sub-proj', subProjectName: 'sub-proj-name' }; - expect(shallow(<ComponentBreadcrumbs branch="feature" issue={issue} />)).toMatchSnapshot(); + const shortBranch: ShortLivingBranch = { + isMain: false, + mergeBranch: '', + name: 'feature', + type: BranchType.SHORT + }; + expect( + shallow(<ComponentBreadcrumbs branchLike={shortBranch} issue={issue} />) + ).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<Props, State> { }; 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 ( <div className="overview-meta-card"> <Button className="js-project-badges" onClick={this.handleOpen}> diff --git a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx index cea857f2e90..d77c077d6d9 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx @@ -21,13 +21,20 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import BadgesModal from '../BadgesModal'; import { click } from '../../../../helpers/testUtils'; +import { ShortLivingBranch, BranchType } from '../../../../app/types'; jest.mock('../../../../helpers/urls', () => ({ getHostUrl: () => 'host' })); it('should display the modal after click', () => { - const wrapper = shallow(<BadgesModal branch="branch-6.6" metrics={{}} project="foo" />); + const shortBranch: ShortLivingBranch = { + isMain: false, + mergeBranch: '', + name: 'branch-6.6', + type: BranchType.SHORT + }; + const wrapper = shallow(<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" />); expect(wrapper).toMatchSnapshot(); click(wrapper.find('Button')); expect(wrapper.find('Modal')).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/overview/badges/utils.ts b/server/sonar-web/src/main/js/apps/overview/badges/utils.ts index 39113f88945..5f300830c1b 100644 --- a/server/sonar-web/src/main/js/apps/overview/badges/utils.ts +++ b/server/sonar-web/src/main/js/apps/overview/badges/utils.ts @@ -28,6 +28,7 @@ export interface BadgeOptions { color?: BadgeColors; project?: string; metric?: string; + pullRequest?: string; } export enum BadgeType { @@ -38,19 +39,19 @@ export enum BadgeType { export function getBadgeUrl( type: BadgeType, - { branch, project, color = 'white', metric = 'alert_status' }: BadgeOptions + { branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions ) { switch (type) { case BadgeType.marketing: return `${getHostUrl()}/images/project_badges/sonarcloud-${color}.svg`; case BadgeType.qualityGate: return `${getHostUrl()}/api/project_badges/quality_gate?${stringify( - omitNil({ branch, project }) + omitNil({ branch, project, pullRequest }) )}`; case BadgeType.measure: default: return `${getHostUrl()}/api/project_badges/measure?${stringify( - omitNil({ branch, project, metric }) + omitNil({ branch, project, metric, pullRequest }) )}`; } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index 03d898d4e9e..0d7caeb5d3e 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -21,12 +21,12 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import OverviewApp from './OverviewApp'; import EmptyOverview from './EmptyOverview'; -import { getBranchName, isShortLivingBranch } from '../../../helpers/branches'; -import { getProjectBranchUrl, getCodeUrl } from '../../../helpers/urls'; -import { Branch, Component } from '../../../app/types'; +import { Component, BranchLike } from '../../../app/types'; +import { isShortLivingBranch } from '../../../helpers/branches'; +import { getShortLivingBranchUrl, getCodeUrl } from '../../../helpers/urls'; interface Props { - branch?: Branch; + branchLike?: BranchLike; component: Component; isInProgress?: boolean; isPending?: boolean; @@ -39,7 +39,7 @@ export default class App extends React.PureComponent<Props> { }; componentDidMount() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; if (this.isPortfolio()) { this.context.router.replace({ @@ -48,10 +48,10 @@ export default class App extends React.PureComponent<Props> { }); } else if (this.isFile()) { this.context.router.replace( - getCodeUrl(component.breadcrumbs[0].key, getBranchName(branch), component.key) + getCodeUrl(component.breadcrumbs[0].key, branchLike, component.key) ); - } else if (isShortLivingBranch(branch)) { - this.context.router.replace(getProjectBranchUrl(component.key, branch)); + } else if (isShortLivingBranch(branchLike)) { + this.context.router.replace(getShortLivingBranchUrl(component.key, branchLike.name)); } } @@ -60,9 +60,9 @@ export default class App extends React.PureComponent<Props> { isFile = () => ['FIL', 'UTS'].includes(this.props.component.qualifier); render() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; - if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branch)) { + if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branchLike)) { return null; } @@ -77,7 +77,7 @@ export default class App extends React.PureComponent<Props> { return ( <OverviewApp - branch={branch} + branchLike={branchLike} component={component} onComponentChange={this.props.onComponentChange} /> diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index 5bc3679f9df..091826a44a6 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -36,14 +36,14 @@ import { getLeakPeriod, Period } from '../../../helpers/periods'; import { getCustomGraph, getGraph } from '../../../helpers/storage'; import { METRICS, HISTORY_METRICS_LIST } from '../utils'; import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; -import { getBranchName } from '../../../helpers/branches'; +import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import { fetchMetrics } from '../../../store/rootActions'; import { getMetrics } from '../../../store/rootReducer'; -import { Branch, Component, Metric } from '../../../app/types'; +import { BranchLike, Component, Metric } from '../../../app/types'; import '../styles.css'; interface OwnProps { - branch?: Branch; + branchLike?: BranchLike; component: Component; onComponentChange: (changes: {}) => void; } @@ -79,7 +79,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { componentDidUpdate(prevProps: Props) { if ( this.props.component.key !== prevProps.component.key || - this.props.branch !== prevProps.branch + !isSameBranchLike(this.props.branchLike, prevProps.branchLike) ) { this.loadMeasures().then(this.loadHistory, () => {}); } @@ -90,12 +90,12 @@ export class OverviewApp extends React.PureComponent<Props, State> { } loadMeasures() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; this.setState({ loading: true }); return getMeasuresAndMeta(component.key, METRICS, { additionalFields: 'metrics,periods', - branch: getBranchName(branch) + ...getBranchLikeQuery(branchLike) }).then( r => { if (this.mounted && r.metrics) { @@ -116,7 +116,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { } loadHistory = () => { - const { branch, component } = this.props; + const { branchLike, component } = this.props; let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); if (!graphMetrics || graphMetrics.length <= 0) { @@ -124,22 +124,24 @@ export class OverviewApp extends React.PureComponent<Props, State> { } const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); - return getAllTimeMachineData(component.key, metrics, { branch: getBranchName(branch) }).then( - r => { - if (this.mounted) { - const history: History = {}; - r.measures.forEach(measure => { - const measureHistory = measure.history.map(analysis => ({ - date: parseDate(analysis.date), - value: analysis.value - })); - history[measure.metric] = measureHistory; - }); - const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; - this.setState({ history, historyStartDate }); - } + return getAllTimeMachineData({ + ...getBranchLikeQuery(branchLike), + component: component.key, + metrics: metrics.join() + }).then(r => { + if (this.mounted) { + const history: History = {}; + r.measures.forEach(measure => { + const measureHistory = measure.history.map(analysis => ({ + date: parseDate(analysis.date), + value: analysis.value + })); + history[measure.metric] = measureHistory; + }); + const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date; + this.setState({ history, historyStartDate }); } - ); + }); }; getApplicationLeakPeriod = () => @@ -156,7 +158,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { } render() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; const { loading, measures, periods, history, historyStartDate } = this.state; if (loading) { @@ -165,9 +167,8 @@ export class OverviewApp extends React.PureComponent<Props, State> { const leakPeriod = component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods); - const branchName = getBranchName(branch); const domainProps = { - branch: branchName, + branchLike, component, measures, leakPeriod, @@ -182,7 +183,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { {component.qualifier === 'APP' ? ( <ApplicationQualityGate component={component} /> ) : ( - <QualityGate branch={branchName} component={component} measures={measures} /> + <QualityGate branchLike={branchLike} component={component} measures={measures} /> )} <div className="overview-domains-list"> @@ -195,7 +196,7 @@ export class OverviewApp extends React.PureComponent<Props, State> { <div className="overview-sidebar page-sidebar-fixed"> <Meta - branch={branchName} + branchLike={branchLike} component={component} history={history} measures={measures} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index 4edd63ffba8..f3eb8d43cba 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -54,7 +54,7 @@ it('redirects on Code page for files', () => { qualifier: 'FIL' }; const replace = jest.fn(); - mount(<App branch={branch} component={newComponent} onComponentChange={jest.fn()} />, { + mount(<App branchLike={branch} component={newComponent} onComponentChange={jest.fn()} />, { context: { router: { replace } } }); expect(replace).toBeCalledWith({ diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx index 8380a53b0a5..d405662ace4 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx @@ -23,11 +23,13 @@ import Analysis from './Analysis'; import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity'; import PreviewGraph from '../../../components/preview-graph/PreviewGraph'; import { translate } from '../../../helpers/l10n'; -import { Metric, Component } from '../../../app/types'; +import { Metric, Component, BranchLike } from '../../../app/types'; import { History } from '../../../api/time-machine'; +import { getBranchLikeQuery } from '../../../helpers/branches'; +import { getActivityUrl } from '../../../helpers/urls'; interface Props { - branch?: string; + branchLike?: BranchLike; component: Component; history?: History; metrics: { [key: string]: Metric }; @@ -76,7 +78,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> { this.setState({ loading: true }); getProjectActivity({ - branch: this.props.branch, + ...getBranchLikeQuery(this.props.branchLike), project: this.getTopLevelComponent(), ps: PAGE_SIZE }).then( @@ -101,7 +103,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> { return ( <ul className="spacer-top"> {analyses.map(analysis => ( - <Analysis key={analysis.key} analysis={analysis} qualifier={this.props.qualifier} /> + <Analysis analysis={analysis} key={analysis.key} qualifier={this.props.qualifier} /> ))} </ul> ); @@ -121,20 +123,16 @@ export default class AnalysesList extends React.PureComponent<Props, State> { </h4> <PreviewGraph - branch={this.props.branch} + branchLike={this.props.branchLike} history={this.props.history} - project={this.props.component.key} metrics={this.props.metrics} + project={this.props.component.key} /> {this.renderList(analyses)} <div className="spacer-top small"> - <Link - to={{ - pathname: '/project/activity', - query: { id: this.props.component.key, branch: this.props.branch } - }}> + <Link to={getActivityUrl(this.props.component.key, this.props.branchLike)}> {translate('show_more')} </Link> </div> diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx new file mode 100644 index 00000000000..710f2608a55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import AnalysesList from '../AnalysesList'; + +it('should render show more link', () => { + const branchLike = { analysisDate: '2018-03-08T09:49:22+0100', isMain: true, name: 'master' }; + const component = { + breadcrumbs: [{ key: 'foo', name: 'foo', qualifier: 'TRK' }], + key: 'foo', + name: 'foo', + organization: 'org', + qualifier: 'TRK' + }; + const wrapper = shallow( + <AnalysesList branchLike={branchLike} component={component} metrics={{}} qualifier="TRK" /> + ); + wrapper.setState({ loading: false }); + expect(wrapper.find('Link')).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap new file mode 100644 index 00000000000..2320d0d0c4f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render show more link 1`] = ` +<Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/activity", + "query": Object { + "id": "foo", + }, + } + } +> + show_more +</Link> +`; diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx index d6433a10e08..52fd2d5253b 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx @@ -31,7 +31,7 @@ import { translate } from '../../../helpers/l10n'; export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { renderHeader() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; return ( <div className="overview-card-header"> @@ -39,13 +39,13 @@ export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { <span>{translate('metric.bugs.name')}</span> <Link className="button button-small spacer-left text-text-bottom" - to={getComponentDrilldownUrl(component.key, 'Reliability', branch)}> + to={getComponentDrilldownUrl(component.key, 'Reliability', branchLike)}> <BubblesIcon size={14} /> </Link> <span className="big-spacer-left">{translate('metric.vulnerabilities.name')}</span> <Link className="button button-small spacer-left text-text-bottom" - to={getComponentDrilldownUrl(component.key, 'Security', branch)}> + to={getComponentDrilldownUrl(component.key, 'Security', branchLike)}> <BubblesIcon size={14} /> </Link> </div> diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.tsx b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.tsx index ac7fcd0c94c..bc632fae96a 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.tsx @@ -26,6 +26,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { getComponentIssuesUrl } from '../../../helpers/urls'; import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon'; +import { getBranchLikeQuery } from '../../../helpers/branches'; export class CodeSmells extends React.PureComponent<ComposedProps> { renderHeader() { @@ -33,10 +34,15 @@ export class CodeSmells extends React.PureComponent<ComposedProps> { } renderDebt(metric: string, type: string) { - const { branch, measures, component } = this.props; + const { branchLike, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metric); const value = measure ? this.props.getValue(measure) : undefined; - const params = { branch, resolved: 'false', facetMode: 'effort', types: type }; + const params = { + ...getBranchLikeQuery(branchLike), + resolved: 'false', + facetMode: 'effort', + types: type + }; if (isDiffMetric(metric)) { Object.assign(params, { sinceLeakPeriod: 'true' }); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx b/server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx index cd1abc807ee..28e5a141ef7 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx @@ -44,7 +44,7 @@ export class Coverage extends React.PureComponent<ComposedProps> { } renderCoverage() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; const metric = 'coverage'; const coverage = this.getCoverage(); @@ -56,7 +56,7 @@ export class Coverage extends React.PureComponent<ComposedProps> { <div className="display-inline-block text-middle"> <div className="overview-domain-measure-value"> - <DrilldownLink branch={branch} component={component.key} metric={metric}> + <DrilldownLink branchLike={branchLike} component={component.key} metric={metric}> <span className="js-overview-main-coverage"> {formatMeasure(coverage, 'PERCENT')} </span> @@ -73,7 +73,7 @@ export class Coverage extends React.PureComponent<ComposedProps> { } renderNewCoverage() { - const { branch, component, leakPeriod, measures } = this.props; + const { branchLike, component, leakPeriod, measures } = this.props; if (!leakPeriod) { return null; } @@ -85,7 +85,7 @@ export class Coverage extends React.PureComponent<ComposedProps> { newCoverageMeasure && newCoverageValue !== undefined ? ( <div> <DrilldownLink - branch={branch} + branchLike={branchLike} component={component.key} metric={newCoverageMeasure.metric.key}> <span className="js-overview-main-new-coverage"> @@ -106,7 +106,7 @@ export class Coverage extends React.PureComponent<ComposedProps> { {translate('overview.coverage_on')} <br /> <DrilldownLink - branch={branch} + branchLike={branchLike} className="spacer-right overview-domain-secondary-measure-value" component={component.key} metric={newLinesToCover.metric.key}> diff --git a/server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx b/server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx index d8a09caee40..49db5f74c36 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx @@ -39,8 +39,7 @@ export class Duplications extends React.PureComponent<ComposedProps> { } renderDuplications() { - const { branch, component, measures } = this.props; - + const { branchLike, component, measures } = this.props; const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density'); if (!measure) { return null; @@ -57,7 +56,7 @@ export class Duplications extends React.PureComponent<ComposedProps> { <div className="display-inline-block text-middle"> <div className="overview-domain-measure-value"> <DrilldownLink - branch={branch} + branchLike={branchLike} component={component.key} metric="duplicated_lines_density"> {formatMeasure(duplications, 'PERCENT')} @@ -74,11 +73,10 @@ export class Duplications extends React.PureComponent<ComposedProps> { } renderNewDuplications() { - const { branch, component, measures, leakPeriod } = this.props; + const { branchLike, component, measures, leakPeriod } = this.props; if (!leakPeriod) { return null; } - const newDuplicationsMeasure = measures.find( measure => measure.metric.key === 'new_duplicated_lines_density' ); @@ -88,7 +86,7 @@ export class Duplications extends React.PureComponent<ComposedProps> { newDuplicationsMeasure && newDuplicationsValue ? ( <div> <DrilldownLink - branch={branch} + branchLike={branchLike} component={component.key} metric={newDuplicationsMeasure.metric.key}> <span className="js-overview-main-new-duplications"> @@ -108,7 +106,7 @@ export class Duplications extends React.PureComponent<ComposedProps> { {translate('overview.duplications_on')} <br /> <DrilldownLink - branch={branch} + branchLike={branchLike} className="spacer-right overview-domain-secondary-measure-value" component={component.key} metric={newLinesMeasure.metric.key}> diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx b/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx index 2b07c3c9239..2251612e2e3 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.tsx @@ -40,11 +40,12 @@ import { getComponentIssuesUrl, getMeasureHistoryUrl } from '../../../helpers/urls'; -import { Component } from '../../../app/types'; +import { Component, BranchLike } from '../../../app/types'; import { History } from '../../../api/time-machine'; +import { getBranchLikeQuery } from '../../../helpers/branches'; export interface EnhanceProps { - branch?: string; + branchLike?: BranchLike; component: Component; measures: MeasureEnhanced[]; leakPeriod?: { index: number; date?: string }; @@ -77,14 +78,14 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP }; renderHeader = (domain: string, label: string) => { - const { branch, component } = this.props; + const { branchLike, component } = this.props; return ( <div className="overview-card-header"> <div className="overview-title"> <span>{label}</span> <Link className="button button-small spacer-left text-text-bottom" - to={getComponentDrilldownUrl(component.key, domain, branch)}> + to={getComponentDrilldownUrl(component.key, domain, branchLike)}> <BubblesIcon size={14} /> </Link> </div> @@ -93,7 +94,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP }; renderMeasure = (metricKey: string) => { - const { branch, measures, component } = this.props; + const { branchLike, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metricKey); if (!measure) { return null; @@ -102,7 +103,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP return ( <div className="overview-domain-measure"> <div className="overview-domain-measure-value"> - <DrilldownLink branch={branch} component={component.key} metric={metricKey}> + <DrilldownLink branchLike={branchLike} component={component.key} metric={metricKey}> <span className="js-overview-main-tests"> {formatMeasure(measure.value, getShortType(measure.metric.type))} </span> @@ -118,7 +119,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP }; renderRating = (metricKey: string) => { - const { branch, component, measures } = this.props; + const { branchLike, component, measures } = this.props; const measure = measures.find(measure => measure.metric.key === metricKey); if (!measure) { return null; @@ -130,7 +131,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP <Tooltip overlay={title} placement="top"> <div className="overview-domain-measure-sup"> <DrilldownLink - branch={branch} + branchLike={branchLike} className="link-no-underline" component={component.key} metric={metricKey}> @@ -142,14 +143,14 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP }; renderIssues = (metric: string, type: string) => { - const { branch, measures, component } = this.props; + const { branchLike, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metric); if (!measure) { return null; } const value = this.getValue(measure); - const params = { branch, resolved: 'false', types: type }; + const params = { ...getBranchLikeQuery(branchLike), resolved: 'false', types: type }; if (isDiffMetric(metric)) { Object.assign(params, { sinceLeakPeriod: 'true' }); } @@ -166,7 +167,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP return ( <Link className={linkClass} - to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branch)}> + to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branchLike)}> <HistoryIcon /> </Link> ); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx index 4a55b5e29a4..edca9b4c4cb 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx @@ -28,13 +28,13 @@ import MetaSize from './MetaSize'; import MetaTags from './MetaTags'; import BadgesModal from '../badges/BadgesModal'; import AnalysesList from '../events/AnalysesList'; -import { Visibility, Component, Metric } from '../../../app/types'; +import { Visibility, Component, Metric, BranchLike } from '../../../app/types'; import { History } from '../../../api/time-machine'; import { translate } from '../../../helpers/l10n'; import { MeasureEnhanced } from '../../../helpers/measures'; interface Props { - branch?: string; + branchLike?: BranchLike; component: Component; history?: History; measures: MeasureEnhanced[]; @@ -50,7 +50,7 @@ export default class Meta extends React.PureComponent<Props> { render() { const { onSonarCloud, organizationsEnabled } = this.context; - const { branch, component, metrics } = this.props; + const { branchLike, component, metrics } = this.props; const { qualifier, description, qualityProfiles, qualityGate, visibility } = component; const isProject = qualifier === 'TRK'; @@ -66,11 +66,11 @@ export default class Meta extends React.PureComponent<Props> { {isProject && ( <MetaTags component={component} onComponentChange={this.props.onComponentChange} /> )} - <MetaSize branch={branch} component={component} measures={this.props.measures} /> + <MetaSize branchLike={branchLike} component={component} measures={this.props.measures} /> </div> <AnalysesList - branch={branch} + branchLike={branchLike} component={component} history={this.props.history} metrics={metrics} @@ -106,7 +106,9 @@ export default class Meta extends React.PureComponent<Props> { {onSonarCloud && isProject && - !isPrivate && <BadgesModal branch={branch} metrics={metrics} project={component.key} />} + !isPrivate && ( + <BadgesModal branchLike={branchLike} metrics={metrics} project={component.key} /> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx index 335d0b48795..0fe760979f5 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx @@ -25,10 +25,10 @@ import SizeRating from '../../../components/ui/SizeRating'; import { formatMeasure, MeasureEnhanced } from '../../../helpers/measures'; import { getMetricName } from '../helpers/metrics'; import { translate } from '../../../helpers/l10n'; -import { LightComponent } from '../../../app/types'; +import { LightComponent, BranchLike } from '../../../app/types'; interface Props { - branch?: string; + branchLike?: BranchLike; component: LightComponent; measures: MeasureEnhanced[]; } @@ -43,7 +43,10 @@ export default class MetaSize extends React.PureComponent<Props> { <span className="spacer-right"> <SizeRating value={Number(ncloc.value)} /> </span> - <DrilldownLink branch={this.props.branch} component={this.props.component.key} metric="ncloc"> + <DrilldownLink + branchLike={this.props.branchLike} + component={this.props.component.key} + metric="ncloc"> {formatMeasure(ncloc.value, 'SHORT_INT')} </DrilldownLink> <div className="spacer-top text-muted">{getMetricName('ncloc')}</div> @@ -71,7 +74,7 @@ export default class MetaSize extends React.PureComponent<Props> { return projects ? ( <div id="overview-projects" className="overview-meta-size-ncloc is-half-width"> <DrilldownLink - branch={this.props.branch} + branchLike={this.props.branchLike} component={this.props.component.key} metric="projects"> {formatMeasure(projects.value, 'SHORT_INT')} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js index 5c3125aedee..20ab7885146 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -38,13 +38,13 @@ function isProject(component /*: Component */) { /*:: type Props = { - branch?: string, + branchLike?: {id?: string; name: string }, component: Component, measures: MeasuresList }; */ -export default function QualityGate({ branch, component, measures } /*: Props */) { +export default function QualityGate({ branchLike, component, measures } /*: Props */) { const statusMeasure = measures.find(measure => measure.metric.key === 'alert_status'); const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details'); @@ -81,7 +81,11 @@ export default function QualityGate({ branch, component, measures } /*: Props */ )} {conditions.length > 0 && ( - <QualityGateConditions branch={branch} component={component} conditions={conditions} /> + <QualityGateConditions + branchLike={branchLike} + component={component} + conditions={conditions} + /> )} </div> ); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js index 3301c33ef6f..ba6d12fa35d 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js @@ -27,12 +27,13 @@ import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; import { getComponentIssuesUrl } from '../../../helpers/urls'; +import { getBranchLikeQuery } from '../../../helpers/branches'; /*:: import type { Component } from '../types'; */ /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ export default class QualityGateCondition extends React.PureComponent { /*:: props: { - branch?: string, + branchLike?: { id?: string; name: string }, component: Component, condition: { level: string, @@ -54,7 +55,11 @@ export default class QualityGateCondition extends React.PureComponent { } getIssuesUrl = (sinceLeakPeriod /*: boolean */, customQuery /*: {} */) => { - const query /*: Object */ = { resolved: 'false', branch: this.props.branch, ...customQuery }; + const query /*: Object */ = { + resolved: 'false', + ...getBranchLikeQuery(this.props.branchLike), + ...customQuery + }; if (sinceLeakPeriod) { Object.assign(query, { sinceLeakPeriod: 'true' }); } @@ -89,7 +94,7 @@ export default class QualityGateCondition extends React.PureComponent { } wrapWithLink(children /*: React.Element<*> */) { - const { branch, component, condition } = this.props; + const { branchLike, component, condition } = this.props; const className = classNames( 'overview-quality-gate-condition', @@ -114,7 +119,7 @@ export default class QualityGateCondition extends React.PureComponent { </Link> ) : ( <DrilldownLink - branch={branch} + branchLike={branchLike} className={className} component={component.key} metric={condition.measure.metric.key} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js index 8abd92619a1..57dc3002b4d 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js @@ -19,10 +19,12 @@ */ import React from 'react'; import { sortBy } from 'lodash'; +import PropTypes from 'prop-types'; import QualityGateCondition from './QualityGateCondition'; import { ComponentType, ConditionsListType } from '../propTypes'; import { getMeasuresAndMeta } from '../../../api/measures'; import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; +import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; const LEVEL_ORDER = ['ERROR', 'WARN']; @@ -35,7 +37,7 @@ function enhanceConditions(conditions, measures) { export default class QualityGateConditions extends React.PureComponent { static propTypes = { - // branch + branchLike: PropTypes.object, component: ComponentType.isRequired, conditions: ConditionsListType.isRequired }; @@ -51,7 +53,7 @@ export default class QualityGateConditions extends React.PureComponent { componentDidUpdate(prevProps) { if ( - prevProps.branch !== this.props.branch || + !isSameBranchLike(prevProps.branchLike, this.props.branchLike) || prevProps.conditions !== this.props.conditions || prevProps.component !== this.props.component ) { @@ -64,13 +66,13 @@ export default class QualityGateConditions extends React.PureComponent { } loadFailedMeasures() { - const { branch, component, conditions } = this.props; + const { branchLike, component, conditions } = this.props; const failedConditions = conditions.filter(c => c.level !== 'OK'); if (failedConditions.length > 0) { const metrics = failedConditions.map(condition => condition.metric); getMeasuresAndMeta(component.key, metrics, { additionalFields: 'metrics', - branch + ...getBranchLikeQuery(branchLike) }).then(r => { if (this.mounted) { const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap index 64d2bf86cfb..4c5e4ee1a87 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap @@ -9,7 +9,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap index e286b3da99d..900cc34dce2 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap @@ -9,7 +9,6 @@ exports[`new_maintainability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "sinceLeakPeriod": "true", @@ -104,7 +103,6 @@ exports[`new_reliability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -158,7 +156,6 @@ exports[`new_security_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -254,7 +251,6 @@ exports[`reliability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -307,7 +303,6 @@ exports[`security_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -360,7 +355,6 @@ exports[`should work with branch 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": "feature", "id": "abcd-key", "resolved": "false", "sinceLeakPeriod": "true", @@ -413,7 +407,6 @@ exports[`sqale_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "abcd-key", "resolved": "false", "types": "CODE_SMELL", diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx index ddba951e313..01d14bb1807 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx @@ -60,13 +60,13 @@ export default class Activity extends React.PureComponent<Props> { fetchHistory = () => { const { component } = this.props; - let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); + let graphMetrics: string[] = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); if (!graphMetrics || graphMetrics.length <= 0) { graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []); } this.setState({ loading: true }); - return getAllTimeMachineData(component, graphMetrics).then( + return getAllTimeMachineData({ component, metrics: graphMetrics.join() }).then( timeMachine => { if (this.mounted) { const history: History = {}; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index 27d4e2d957b..aa0fd5f0a16 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -81,7 +81,7 @@ export class App extends React.PureComponent<Props, State> { fetchData() { this.setState({ loading: true }); Promise.all([ - getMeasures(this.props.component.key, PORTFOLIO_METRICS), + getMeasures({ componentKey: this.props.component.key, metricKeys: PORTFOLIO_METRICS.join() }), getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20, s: 'qualifier' }) ]).then( ([measures, subComponents]) => { diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx index c8c2e81f295..0b5fb1f1584 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx @@ -67,5 +67,5 @@ it('renders', () => { it('fetches history', () => { mount(<Activity component="foo" metrics={{}} />); - expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']); + expect(getAllTimeMachineData).toBeCalledWith({ component: 'foo', metrics: 'coverage' }); }); diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx index 6cfe8d6890b..4abdc4e056b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx @@ -77,23 +77,11 @@ it('fetches measures and children components', () => { getMeasures.mockClear(); getChildren.mockClear(); mount(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />); - expect(getMeasures).toBeCalledWith('foo', [ - 'projects', - 'ncloc', - 'ncloc_language_distribution', - 'releasability_rating', - 'releasability_effort', - 'sqale_rating', - 'maintainability_rating_effort', - 'reliability_rating', - 'reliability_rating_effort', - 'security_rating', - 'security_rating_effort', - 'last_change_on_releasability_rating', - 'last_change_on_maintainability_rating', - 'last_change_on_security_rating', - 'last_change_on_reliability_rating' - ]); + expect(getMeasures).toBeCalledWith({ + componentKey: 'foo', + metricKeys: + 'projects,ncloc,ncloc_language_distribution,releasability_rating,releasability_effort,sqale_rating,maintainability_rating_effort,reliability_rating,reliability_rating_effort,security_rating,security_rating_effort,last_change_on_releasability_rating,last_change_on_maintainability_rating,last_change_on_security_rating,last_change_on_reliability_rating' + }); expect(getChildren).toBeCalledWith( 'foo', [ diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap index 8918c9e4b99..b278210c01b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap @@ -16,7 +16,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "security_rating", }, diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap index e62b2d558c6..8f25ec024e1 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap @@ -9,7 +9,6 @@ exports[`renders 1`] = ` Object { "pathname": "/project/activity", "query": Object { - "branch": undefined, "custom_metrics": "security_rating", "graph": "custom", "id": "foo", diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap index d8cc0a6fd99..b90b5afa313 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap @@ -9,7 +9,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "security_rating", "view": "treemap", diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap index a97849bb45e..b4afa69ade1 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap @@ -9,7 +9,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "security_rating", }, diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap index c2120360b56..2401a4dc09b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap @@ -17,7 +17,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "alert_status", }, @@ -41,7 +40,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "alert_status", }, diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap index 149c818b286..5c828719017 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap @@ -24,7 +24,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "projects", }, @@ -55,7 +54,6 @@ exports[`renders 1`] = ` Object { "pathname": "/component_measures", "query": Object { - "branch": undefined, "id": "foo", "metric": "ncloc", }, diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index 736ab248060..ceb66108e3c 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -53,7 +53,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, } @@ -141,7 +140,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "barbar", }, } @@ -229,7 +227,6 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "bazbaz", }, } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 51d28646604..54aa9929ecc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -26,7 +26,7 @@ import { getAllTimeMachineData } from '../../../api/time-machine'; import { getAllMetrics } from '../../../api/metrics'; import * as api from '../../../api/projectActivity'; import * as actions from '../actions'; -import { getBranchName } from '../../../helpers/branches'; +import { getBranchLikeQuery } from '../../../helpers/branches'; import { parseDate } from '../../../helpers/dates'; import { getCustomGraph, getGraph } from '../../../helpers/storage'; import { @@ -51,7 +51,7 @@ type Component = { }; type Props = { - branch?: {}, + branchLike?: { id?: string; name: string }, location: { pathname: string, query: RawQuery }, component: Component }; @@ -103,7 +103,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeUrlQuery(newQuery), - branch: getBranchName(this.props.branch) + ...getBranchLikeQuery(this.props.branchLike) } }); } else { @@ -169,12 +169,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { [string]: string } */ ) => { - const parameters = { - project, - p, - ps, - branch: getBranchName(this.props.branch) - }; + const parameters = { project, p, ps, ...getBranchLikeQuery(this.props.branchLike) }; return api .getProjectActivity({ ...additional, ...parameters }) .then(({ analyses, paging }) => ({ @@ -187,8 +182,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent { if (metrics.length <= 0) { return Promise.resolve([]); } - return getAllTimeMachineData(this.props.component.key, metrics, { - branch: getBranchName(this.props.branch) + return getAllTimeMachineData({ + component: this.props.component.key, + metrics: metrics.join(), + ...getBranchLikeQuery(this.props.branchLike) }).then( ({ measures }) => measures.map(measure => ({ @@ -300,7 +297,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { pathname: this.props.location.pathname, query: { ...query, - branch: getBranchName(this.props.branch), + ...getBranchLikeQuery(this.props.branchLike), id: this.props.component.key } }); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx index e4c886668fd..24dac728e91 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx @@ -22,14 +22,19 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import BranchRow from './BranchRow'; import LongBranchesPattern from './LongBranchesPattern'; -import { Branch } from '../../../app/types'; -import { sortBranchesAsTree } from '../../../helpers/branches'; +import { BranchLike } from '../../../app/types'; +import { + sortBranchesAsTree, + getBranchLikeKey, + isShortLivingBranch, + isPullRequest +} from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; import { getValues } from '../../../api/settings'; import { formatMeasure } from '../../../helpers/measures'; interface Props { - branches: Branch[]; + branchLikes: BranchLike[]; canAdmin?: boolean; component: { key: string }; onBranchesChange: () => void; @@ -57,7 +62,7 @@ export default class App extends React.PureComponent<Props, State> { fetchPurgeSetting() { this.setState({ loading: true }); - getValues(BRANCH_LIFETIME_SETTING).then( + getValues({ keys: BRANCH_LIFETIME_SETTING }).then( settings => { if (this.mounted) { this.setState({ @@ -78,26 +83,29 @@ export default class App extends React.PureComponent<Props, State> { return null; } - const messageKey = this.props.canAdmin - ? 'project_branches.page.life_time.admin' - : 'project_branches.page.life_time'; - return ( <p className="page-description"> <FormattedMessage - defaultMessage={translate(messageKey)} - id={messageKey} - values={{ - days: formatMeasure(this.state.branchLifeTime, 'INT'), - settings: <Link to="/admin/settings">{translate('settings.page')}</Link> - }} + defaultMessage={translate('project_branches.page.life_time')} + id="project_branches.page.life_time" + values={{ days: formatMeasure(this.state.branchLifeTime, 'INT') }} /> + {this.props.canAdmin && ( + <> + <br /> + <FormattedMessage + defaultMessage={translate('project_branches.page.life_time.admin')} + id="project_branches.page.life_time.admin" + values={{ settings: <Link to="/admin/settings">{translate('settings.page')}</Link> }} + /> + </> + )} </p> ); } render() { - const { branches, component, onBranchesChange } = this.props; + const { branchLikes, component, onBranchesChange } = this.props; if (this.state.loading) { return ( @@ -132,11 +140,15 @@ export default class App extends React.PureComponent<Props, State> { </tr> </thead> <tbody> - {sortBranchesAsTree(branches).map(branch => ( + {sortBranchesAsTree(branchLikes).map(branchLike => ( <BranchRow - branch={branch} + branchLike={branchLike} component={component.key} - key={branch.name} + isOrphan={ + (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && + branchLike.isOrphan + } + key={getBranchLikeKey(branchLike)} onChange={onBranchesChange} /> ))} 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<Props, State> { }; renderActions() { - const { branch, component } = this.props; + const { branchLike, component } = this.props; return ( <td className="thin nowrap text-right"> <ActionsDropdown className="ig-spacer-left"> - {isLongLivingBranch(branch) && ( + {isLongLivingBranch(branchLike) && ( <ActionsDropdownItem className="js-change-leak-period" onClick={this.handleChangeLeakClick}> {translate('branches.set_leak_period')} </ActionsDropdownItem> )} - {isLongLivingBranch(branch) && !branch.isMain && <ActionsDropdownDivider />} - {branch.isMain ? ( + {isLongLivingBranch(branchLike) && <ActionsDropdownDivider />} + {isMainBranch(branchLike) ? ( <ActionsDropdownItem className="js-rename" onClick={this.handleRenameClick}> {translate('branches.rename')} </ActionsDropdownItem> @@ -112,62 +119,65 @@ export default class BranchRow extends React.PureComponent<Props, State> { className="js-delete" destructive={true} onClick={this.handleDeleteClick}> - {translate('branches.delete')} + {translate( + isPullRequest(branchLike) ? 'branches.pull_request.delete' : 'branches.delete' + )} </ActionsDropdownItem> )} </ActionsDropdown> {this.state.deleting && ( <DeleteBranchModal - branch={branch} + branchLike={branchLike} component={component} onClose={this.handleDeletingStop} onDelete={this.handleChange} /> )} - {this.state.renaming && ( - <RenameBranchModal - branch={branch} - component={component} - onClose={this.handleRenamingStop} - onRename={this.handleChange} - /> - )} + {this.state.renaming && + isMainBranch(branchLike) && ( + <RenameBranchModal + branch={branchLike} + component={component} + onClose={this.handleRenamingStop} + onRename={this.handleChange} + /> + )} - {this.state.changingLeak && ( - <LeakPeriodForm - branch={branch.name} - onClose={this.handleChangingLeakStop} - project={component} - /> - )} + {this.state.changingLeak && + isLongLivingBranch(branchLike) && ( + <LeakPeriodForm + branch={branchLike.name} + onClose={this.handleChangingLeakStop} + project={component} + /> + )} </td> ); } render() { - const { branch } = this.props; + const { branchLike, isOrphan } = this.props; + const indented = (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && !isOrphan; return ( <tr> <td> <BranchIcon - branch={branch} - className={classNames('little-spacer-right', { - 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan - })} + branchLike={branchLike} + className={classNames('little-spacer-right', { 'big-spacer-left': indented })} /> - {branch.name} - {branch.isMain && ( + {getBranchLikeDisplayName(branchLike)} + {isMainBranch(branchLike) && ( <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div> )} </td> <td className="thin nowrap text-right"> - <BranchStatus branch={branch} /> + <BranchStatus branchLike={branchLike} /> </td> <td className="thin nowrap text-right"> - {branch.analysisDate && <DateFromNow date={branch.analysisDate} />} + {branchLike.analysisDate && <DateFromNow date={branchLike.analysisDate} />} </td> {this.renderActions()} </tr> 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<Props, State> handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { 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<Props, State> }; 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 ( <Modal contentLabel={header} onRequestClose={this.props.onClose}> @@ -76,7 +88,12 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State> </header> <form onSubmit={this.handleSubmit}> <div className="modal-body"> - {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) + )} </div> <footer className="modal-foot"> {this.state.loading && <i className="spinner spacer-right" />} 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<Props, State> { 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<Props, Stat } fetchSetting() { - return getValues(LONG_BRANCH_PATTERN, this.props.project).then( + return getValues({ keys: LONG_BRANCH_PATTERN, component: this.props.project }).then( settings => { 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<Props, State> { this.mounted = false; } + stopLoading = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); @@ -65,11 +71,7 @@ export default class SettingForm extends React.PureComponent<Props, State> { 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<HTMLInputElement>) => { @@ -78,14 +80,11 @@ export default class SettingForm extends React.PureComponent<Props, State> { 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<any>; @@ -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( - <App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} /> + <App + branchLikes={branchLikes} + canAdmin={true} + component={{ key: 'foo' }} + onBranchesChange={jest.fn()} + /> ); wrapper.setState({ branchLifeTime: '100', loading: false }); expect(wrapper).toMatchSnapshot(); }); it('fetches branch life time setting on mount', () => { - mount(<App branches={[]} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />); - expect(getValues).toBeCalledWith('sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches'); + mount(<App branchLikes={[]} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />); + 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(<BranchRow branch={branch} component="foo" onChange={onChange} />); +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( + <BranchRow branchLike={branchLike} component="foo" isOrphan={false} onChange={onChange} /> + ); (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<any>).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<any>).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<any>).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( - <DeleteBranchModal branch={branch} component="foo" onClose={onClose} onDelete={onDelete} /> + <DeleteBranchModal + branchLike={branchLike} + component="foo" + onClose={onClose} + onDelete={onDelete} + /> ); (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(<LongBranchesPattern project="project" />); - 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": <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/admin/settings" - > - settings.page - </Link>, } } /> + <React.Fragment> + <br /> + <FormattedMessage + defaultMessage="project_branches.page.life_time.admin" + id="project_branches.page.life_time.admin" + values={ + Object { + "settings": <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/admin/settings" + > + settings.page + </Link>, + } + } + /> + </React.Fragment> </p> </header> <div @@ -71,31 +82,45 @@ exports[`renders sorted list of branches 1`] = ` </thead> <tbody> <BranchRow - branch={ + branchLike={ Object { "isMain": true, "name": "master", } } component="foo" - key="master" + isOrphan={false} + key="branch-master" + onChange={[MockFunction]} + /> + <BranchRow + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + } + } + component="foo" + key="pull-request-1234" onChange={[MockFunction]} /> <BranchRow - branch={ + branchLike={ Object { "isMain": false, "mergeBranch": "master", - "name": "branch-1.0", + "name": "feature", "type": "SHORT", } } component="foo" - key="branch-1.0" + key="branch-feature" onChange={[MockFunction]} /> <BranchRow - branch={ + branchLike={ Object { "isMain": false, "name": "branch-1.0", @@ -103,7 +128,8 @@ exports[`renders sorted list of branches 1`] = ` } } component="foo" - key="branch-1.0" + isOrphan={false} + key="branch-branch-1.0" onChange={[MockFunction]} /> </tbody> 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`] = ` <tr> <td> <BranchIcon - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -23,7 +23,7 @@ exports[`renders main branch 1`] = ` className="thin nowrap text-right" > <BranchStatus - branch={ + branchLike={ Object { "isMain": true, "name": "master", @@ -51,11 +51,62 @@ exports[`renders main branch 1`] = ` </tr> `; +exports[`renders pull request 1`] = ` +<tr> + <td> + <BranchIcon + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + } + } + className="little-spacer-right big-spacer-left" + /> + 1234 – Feature PR + </td> + <td + className="thin nowrap text-right" + > + <BranchStatus + branchLike={ + Object { + "base": "master", + "branch": "feature", + "key": "1234", + "title": "Feature PR", + } + } + /> + </td> + <td + className="thin nowrap text-right" + /> + <td + className="thin nowrap text-right" + > + <ActionsDropdown + className="ig-spacer-left" + > + <ActionsDropdownItem + className="js-delete" + destructive={true} + onClick={[Function]} + > + branches.pull_request.delete + </ActionsDropdownItem> + </ActionsDropdown> + </td> +</tr> +`; + exports[`renders short-living branch 1`] = ` <tr> <td> <BranchIcon - branch={ + branchLike={ Object { "analysisDate": "2017-09-27T00:05:19+0000", "isMain": false, @@ -72,7 +123,7 @@ exports[`renders short-living branch 1`] = ` className="thin nowrap text-right" > <BranchStatus - branch={ + branchLike={ Object { "analysisDate": "2017-09-27T00:05:19+0000", "isMain": false, diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap index 505f5276139..733d95af43f 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap @@ -18,7 +18,6 @@ exports[`renders 1`] = ` "link": Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, }, diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap index afb4e501021..b6e9c87e444 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap @@ -18,7 +18,6 @@ exports[`renders 1`] = ` "link": Object { "pathname": "/dashboard", "query": Object { - "branch": undefined, "id": "foo", }, }, diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index b43fd2d96fa..2c58557ac4f 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -57,8 +57,8 @@ export default class ProjectRowActions extends React.PureComponent<Props, State> // 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<SourceViewerFile>; + loadComponent?: ( + component: string, + branchLike: BranchLike | undefined + ) => Promise<SourceViewerFile>; loadIssues?: ( component: string, from: number, to: number, - branch: string | undefined + branchLike: BranchLike | undefined ) => Promise<Issue[]>; loadSources?: ( component: string, from: number, to: number, - branch: string | undefined + branchLike: BranchLike | undefined ) => Promise<SourceLine[]>; onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; onLocationSelect?: (index: number) => void; @@ -162,7 +167,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> } 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<Props, State> 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<Props, State> ); }; - 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<Props, State> 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<Props, State> // 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<Props, State> 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<Props, State> 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<Props, State> }; 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<Props, State> return ( <DuplicationPopup blocks={blocks} - branch={this.props.branch} + branchLike={this.props.branchLike} duplicatedFiles={duplicatedFiles} inRemovedComponent={inRemovedComponent} onClose={this.closeLinePopup} @@ -628,7 +639,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; return ( <SourceViewerCode - branch={this.props.branch} + branchLike={this.props.branchLike} componentKey={this.props.component} displayAllIssues={this.props.displayAllIssues} displayIssueLocationsCount={this.props.displayIssueLocationsCount} @@ -706,7 +717,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> return ( <div className={className} ref={node => (this.node = node)}> {this.state.component && ( - <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} /> + <SourceViewerHeader + branchLike={this.props.branchLike} + sourceViewerFile={this.state.component} + /> )} {sourceRemoved && ( <div className="alert alert-warning spacer-top"> @@ -719,10 +733,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> } } -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<Props> { return ( <Line - branch={this.props.branch} + branchLike={this.props.branchLike} componentKey={this.props.componentKey} displayAllIssues={this.props.displayAllIssues} displayCoverage={displayCoverage} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index a29348b9312..0ba3cd6ec40 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -17,24 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { stringify } from 'querystring'; import * as React from 'react'; import { Link } from 'react-router'; import MeasuresOverlay from './components/MeasuresOverlay'; -import { SourceViewerFile } from '../../app/types'; +import { SourceViewerFile, BranchLike } from '../../app/types'; import QualifierIcon from '../shared/QualifierIcon'; import FavoriteContainer from '../controls/FavoriteContainer'; import { getPathUrlAsString, - getProjectUrl, + getBranchLikeUrl, getComponentIssuesUrl, getBaseUrl } from '../../helpers/urls'; import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; import { translate } from '../../helpers/l10n'; +import { getBranchLikeQuery } from '../../helpers/branches'; import { formatMeasure } from '../../helpers/measures'; +import { omitNil } from '../../helpers/request'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; sourceViewerFile: SourceViewerFile; } @@ -58,7 +61,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State event.preventDefault(); const { key } = this.props.sourceViewerFile; const Workspace = require('../workspace/main').default; - Workspace.openComponent({ key, branch: this.props.branch }); + Workspace.openComponent({ key, branchLike: this.props.branchLike }); }; render() { @@ -75,11 +78,10 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State } = this.props.sourceViewerFile; const isUnitTest = q === 'UTS'; const workspace = false; - let rawSourcesLink = - getBaseUrl() + `/api/sources/raw?key=${encodeURIComponent(this.props.sourceViewerFile.key)}`; - if (this.props.branch) { - rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; - } + const rawSourcesLink = + getBaseUrl() + + '/api/sources/raw?' + + stringify(omitNil({ key, ...getBranchLikeQuery(this.props.branchLike) })); // TODO favorite return ( @@ -89,7 +91,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State <div className="component-name-parent"> <a className="link-with-icon" - href={getPathUrlAsString(getProjectUrl(project, this.props.branch))}> + href={getPathUrlAsString(getBranchLikeUrl(project, this.props.branchLike))}> <QualifierIcon qualifier="TRK" /> <span>{projectName}</span> </a> </div> @@ -98,7 +100,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State <div className="component-name-parent"> <a className="link-with-icon" - href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))}> + href={getPathUrlAsString(getBranchLikeUrl(subProject, this.props.branchLike))}> <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span> </a> </div> @@ -127,7 +129,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State </a> {this.state.measuresOverlay && ( <MeasuresOverlay - branch={this.props.branch} + branchLike={this.props.branchLike} onClose={this.handleMeasuresOverlayClose} sourceViewerFile={this.props.sourceViewerFile} /> @@ -138,7 +140,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State className="js-new-window" href={getPathUrlAsString({ pathname: '/component', - query: { branch: this.props.branch, id: this.props.sourceViewerFile.key } + query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } })} target="_blank"> {translate('component_viewer.new_window')} @@ -188,7 +190,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid, - branch: this.props.branch + ...getBranchLikeQuery(this.props.branchLike) })}> {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} </Link> 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<Props, State> { } 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<Props, State> { 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<Props, State> { 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<Props> { 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<Props> { return ( <tr className={className} data-line-number={line.line}> <LineNumber - branch={this.props.branch} + branchLike={this.props.branchLike} componentKey={this.props.componentKey} line={line} onPopupToggle={this.props.onLinePopupToggle} @@ -137,7 +137,7 @@ export default class Line extends React.PureComponent<Props> { {this.props.displayCoverage && ( <LineCoverage - branch={this.props.branch} + branchLike={this.props.branchLike} componentKey={this.props.componentKey} line={line} onPopupToggle={this.props.onLinePopupToggle} @@ -171,7 +171,7 @@ export default class Line extends React.PureComponent<Props> { )} <LineCode - branch={this.props.branch} + branchLike={this.props.branchLike} displayIssueLocationsCount={this.props.displayIssueLocationsCount} displayIssueLocationsLink={this.props.displayIssueLocationsLink} displayLocationMarkers={this.props.displayLocationMarkers} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index c91f58c43c2..9becbdeb876 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import LineIssuesList from './LineIssuesList'; -import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; +import { BranchLike, Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; import LocationIndex from '../../common/LocationIndex'; import LocationMessage from '../../common/LocationMessage'; import { @@ -31,7 +31,7 @@ import { } from '../helpers/highlight'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; displayIssueLocationsCount?: boolean; displayIssueLocationsLink?: boolean; displayLocationMarkers?: boolean; @@ -231,7 +231,7 @@ export default class LineCode extends React.PureComponent<Props, State> { {showIssues && issues.length > 0 && ( <LineIssuesList - branch={this.props.branch} + branchLike={this.props.branchLike} displayIssueLocationsCount={this.props.displayIssueLocationsCount} displayIssueLocationsLink={this.props.displayIssueLocationsLink} issuePopup={this.props.issuePopup} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx index c6b9efb895d..17d98a03eca 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import CoveragePopup from './CoveragePopup'; -import { SourceLine } from '../../../app/types'; +import { BranchLike, SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; import { translate } from '../../../helpers/l10n'; import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; @@ -49,7 +49,7 @@ export default class LineCoverage extends React.PureComponent<Props> { }; 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<Props> { isOpen={popupOpen} popup={ <CoveragePopup - branch={branch} + branchLike={branchLike} componentKey={componentKey} line={line} onClose={this.closePopup} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx index 231cc639171..6e8cfca7732 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx @@ -18,11 +18,11 @@ * 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'; import Issue from '../../issue/Issue'; interface Props { - branch: string | undefined; + branchLike: BranchLike | undefined; displayIssueLocationsCount?: boolean; displayIssueLocationsLink?: boolean; issuePopup: { issue: string; name: string } | undefined; @@ -40,7 +40,7 @@ export default function LineIssuesList(props: Props) { <div className="issue-list"> {props.issues.map(issue => ( <Issue - branch={props.branch} + branchLike={props.branchLike} displayLocationsCount={props.displayIssueLocationsCount} displayLocationsLink={props.displayIssueLocationsLink} issue={issue} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx index f6126f5e7ec..6d1f4940141 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -19,12 +19,11 @@ */ import * as React from 'react'; import LineOptionsPopup from './LineOptionsPopup'; -import { SourceLine } from '../../../app/types'; +import { BranchLike, SourceLine } from '../../../app/types'; import BubblePopupHelper from '../../common/BubblePopupHelper'; interface Props { - // TODO use branchLike - branch: string | undefined; + branchLike: BranchLike | undefined; componentKey: string; line: SourceLine; onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; @@ -44,7 +43,7 @@ export default class LineNumber extends React.PureComponent<Props> { }; 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<Props> { tabIndex={0}> <BubblePopupHelper isOpen={popupOpen} - popup={<LineOptionsPopup branch={branch} componentKey={componentKey} line={line} />} + popup={ + <LineOptionsPopup branchLike={branchLike} componentKey={componentKey} line={line} /> + } 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 ( <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}> 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<Props, State> { 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<Props, State> { }; 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<Props, State> { <div className="source-viewer-header-component source-viewer-measures-component"> <div className="source-viewer-header-component-project"> <QualifierIcon className="little-spacer-right" qualifier="TRK" /> - <Link to={getProjectUrl(sourceViewerFile.project, branch)}> + <Link to={getBranchLikeUrl(sourceViewerFile.project, branchLike)}> {sourceViewerFile.projectName} </Link> {sourceViewerFile.subProject && ( <> <QualifierIcon className="big-spacer-left little-spacer-right" qualifier="BRC" /> - <Link to={getProjectUrl(sourceViewerFile.subProject, branch)}> + <Link to={getBranchLikeUrl(sourceViewerFile.subProject, branchLike)}> {sourceViewerFile.subProjectName} </Link> </> @@ -419,7 +422,10 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> { {sourceViewerFile.q === 'UTS' ? ( <> {this.renderTests()} - <MeasuresOverlayTestCases branch={branch} componentKey={sourceViewerFile.key} /> + <MeasuresOverlayTestCases + branchLike={branchLike} + componentKey={sourceViewerFile.key} + /> </> ) : ( <div className="source-viewer-measures"> 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<Props, componentDidUpdate(prevProps: Props) { if ( - prevProps.branch !== this.props.branch || + prevProps.branchLike !== this.props.branchLike || prevProps.componentKey !== this.props.componentKey ) { this.fetchTests(); @@ -64,7 +65,11 @@ export default class MeasuresOverlayTestCases extends React.PureComponent<Props, fetchTests = () => { // 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: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {' }; const issueLocations = [{ from: 0, to: 5, line: 3 }]; + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT + }; const wrapper = shallow( <LineCode - branch="feature" + branchLike={branch} highlightedLocationMessage={undefined} highlightedSymbols={['sym1']} issueLocations={issueLocations} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx index a737504b3bd..efbe7ecfe67 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx @@ -27,7 +27,7 @@ it('render covered line', () => { const line: SourceLine = { line: 3, coverageStatus: 'covered' }; const wrapper = shallow( <LineCoverage - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={jest.fn()} @@ -42,7 +42,7 @@ it('render uncovered line', () => { const line: SourceLine = { line: 3, coverageStatus: 'uncovered' }; const wrapper = shallow( <LineCoverage - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={jest.fn()} @@ -56,7 +56,7 @@ it('render line with unknown coverage', () => { const line: SourceLine = { line: 3 }; const wrapper = shallow( <LineCoverage - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={jest.fn()} @@ -71,7 +71,7 @@ it('should open coverage popup', () => { const onPopupToggle = jest.fn(); const wrapper = shallow( <LineCoverage - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={onPopupToggle} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx index 8ab04fcda21..7e9c9ec5bbe 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx @@ -49,7 +49,7 @@ it('render issues list', () => { const onIssueClick = jest.fn(); const wrapper = shallow( <LineIssuesList - branch={undefined} + branchLike={undefined} issuePopup={undefined} issues={issues} onIssueChange={jest.fn()} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx index bf40ca900c8..d027d938756 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx @@ -26,7 +26,7 @@ it('render line 3', () => { const line = { line: 3 }; const wrapper = shallow( <LineNumber - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={jest.fn()} @@ -41,7 +41,7 @@ it('render line 0', () => { const line = { line: 0 }; const wrapper = shallow( <LineNumber - branch={undefined} + branchLike={undefined} componentKey="foo" line={line} onPopupToggle={jest.fn()} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx index a5de2ec635e..5ada553fbd6 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx @@ -20,9 +20,16 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import LineOptionsPopup from '../LineOptionsPopup'; +import { BranchType, ShortLivingBranch } from '../../../../app/types'; it('should render', () => { const line = { line: 3 }; - const wrapper = shallow(<LineOptionsPopup branch="feature" componentKey="foo" line={line} />); + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + type: BranchType.SHORT + }; + const wrapper = shallow(<LineOptionsPopup branchLike={branch} componentKey="foo" line={line} />); 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( - <MeasuresOverlay branch="branch" onClose={jest.fn()} sourceViewerFile={sourceViewerFile} /> + <MeasuresOverlay + branchLike={branchLike} + onClose={jest.fn()} + sourceViewerFile={sourceViewerFile} + /> ); 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( <MeasuresOverlay - branch="branch" + branchLike={branchLike} onClose={jest.fn()} sourceViewerFile={{ ...sourceViewerFile, q: 'UTS' }} /> 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( - <MeasuresOverlayTestCases branch="branch" componentKey="component-key" /> + <MeasuresOverlayTestCases branchLike={branchLike} componentKey="component-key" /> ); 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`] = ` </pre> </div> <LineIssuesList - branch="feature" + branchLike={ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "feature", + "type": "SHORT", + } + } issues={ Array [ Object { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap index 664a60ddf64..4e01802b363 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap @@ -20,7 +20,7 @@ exports[`render covered line 1`] = ` isOpen={false} popup={ <CoveragePopup - branch={undefined} + branchLike={undefined} componentKey="foo" line={ Object { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap index 477d94d1c3f..2633afb6d2c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap @@ -18,7 +18,7 @@ exports[`render line 3 1`] = ` isOpen={false} popup={ <LineOptionsPopup - branch={undefined} + branchLike={undefined} componentKey="foo" line={ Object { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap index 7378602ea6b..50bdfbabfcd 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap @@ -24,10 +24,11 @@ exports[`should render source file 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "project-key", + "resolved": "false", }, } } @@ -44,10 +45,11 @@ exports[`should render source file 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "sub-project-key", + "resolved": "false", }, } } @@ -401,10 +403,11 @@ exports[`should render source file 2`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "project-key", + "resolved": "false", }, } } @@ -421,10 +424,11 @@ exports[`should render source file 2`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "sub-project-key", + "resolved": "false", }, } } @@ -1384,10 +1388,11 @@ exports[`should render test file 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "project-key", + "resolved": "false", }, } } @@ -1404,10 +1409,11 @@ exports[`should render test file 1`] = ` style={Object {}} to={ Object { - "pathname": "/dashboard", + "pathname": "/project/issues", "query": Object { - "branch": "branch", + "branch": "feature", "id": "sub-project-key", + "resolved": "false", }, } } @@ -1501,7 +1507,14 @@ exports[`should render test file 1`] = ` </div> </div> <MeasuresOverlayTestCases - branch="branch" + branchLike={ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "feature", + "type": "SHORT", + } + } componentKey="component-key" /> </React.Fragment> 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<Issue[]> { - 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) { <StatusIndicator color={indicatorColor} size="small" /> </li> <li className="spacer-left"> - {branch.status.bugs} + {branchLike.status.bugs} <BugIcon /> </li> <li className="spacer-left"> - {branch.status.vulnerabilities} + {branchLike.status.vulnerabilities} <VulnerabilityIcon /> </li> <li className="spacer-left"> - {branch.status.codeSmells} + {branchLike.status.codeSmells} <CodeSmellIcon /> </li> </ul> ); - } else { - if (!branch.status) { + } else if (isLongLivingBranch(branchLike)) { + if (!branchLike.status) { return null; } - return <Level level={branch.status.qualityGateStatus} small={true} />; + return <Level level={branchLike.status.qualityGateStatus} small={true} />; + } 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(<BranchStatus branch={branch} />)).toMatchSnapshot(); + expect(shallow(<BranchStatus branchLike={shortBranch} />)).toMatchSnapshot(); } }); @@ -53,6 +53,6 @@ it('renders status of long-living branches', () => { if (qualityGateStatus) { branch.status = { qualityGateStatus }; } - return shallow(<BranchStatus branch={branch} />); + return shallow(<BranchStatus branchLike={branch} />); } }); 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) ? ( - <ShortLivingBranchIcon {...props} /> - ) : ( - <LongLivingBranchIcon {...props} /> - ); +export default function BranchIcon({ branchLike, ...props }: Props) { + if (isPullRequest(branchLike)) { + return <PullRequestIcon {...props} />; + } else if (isShortLivingBranch(branchLike)) { + return <ShortLivingBranchIcon {...props} />; + } else { + return <LongLivingBranchIcon {...props} />; + } } 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' }}> <path style={{ fill }} - d="M3 11.9V4.1c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2c0 .9.6 1.7 1.5 1.9v7.8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zM1.5 2.2c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm1 12.7c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zM14 11.9V5.5c0-.1-.2-3.1-5.1-3.5L10.1.8 9.5.1 6.9 2.6l2.6 2.5.7-.7L8.8 3c4 .2 4.2 2.4 4.2 2.5v6.4c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zm-.5 3c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z" + d="M13,11.9L13,5.5C13,5.4 13.232,1.996 7.9,2L9.1,0.8L8.5,0.1L5.9,2.6L8.5,5.1L9.2,4.4L7.905,3.008C12.256,2.99 12,5.4 12,5.5L12,11.9C11.1,12.1 10.5,12.9 10.5,13.8C10.5,14.9 11.4,15.8 12.5,15.8C13.6,15.8 14.5,14.9 14.5,13.8C14.5,12.9 13.9,12.2 13,11.9ZM4,11.9C4.9,12.2 5.5,12.9 5.5,13.8C5.5,14.9 4.6,15.8 3.5,15.8C2.4,15.8 1.5,14.9 1.5,13.8C1.5,12.9 2.1,12.1 3,11.9L3,4.1C2.1,3.9 1.5,3.1 1.5,2.2C1.5,1.1 2.4,0.2 3.5,0.2C4.6,0.2 5.5,1.1 5.5,2.2C5.5,3.1 4.9,3.9 4,4.1L4,11.9ZM12.5,14.9C11.9,14.9 11.5,14.5 11.5,13.9C11.5,13.3 11.9,12.9 12.5,12.9C13.1,12.9 13.5,13.3 13.5,13.9C13.5,14.5 13.1,14.9 12.5,14.9ZM3.5,14.9C2.9,14.9 2.5,14.5 2.5,13.9C2.5,13.3 2.9,12.9 3.5,12.9C4.1,12.9 4.5,13.3 4.5,13.9C4.5,14.5 4.1,14.9 3.5,14.9ZM2.5,2.2C2.5,1.6 2.9,1.2 3.5,1.2C4.1,1.2 4.5,1.6 4.5,2.2C4.5,2.8 4.1,3.2 3.5,3.2C2.9,3.2 2.5,2.8 2.5,2.2Z" /> </svg> ); 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 ( <IssueView - branch={this.props.branch} + branchLike={this.props.branchLike} checked={this.props.checked} currentPopup={this.props.openPopup} displayLocationsCount={this.props.displayLocationsCount} diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js index b7a658e77ba..5f814a5347e 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.js +++ b/server/sonar-web/src/main/js/components/issue/IssueView.js @@ -29,7 +29,7 @@ import { deleteIssueComment, editIssueComment } from '../../api/issues'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, checked?: boolean, currentPopup: ?string, displayLocationsCount?: boolean; @@ -90,7 +90,7 @@ export default class IssueView extends React.PureComponent { role="listitem" tabIndex={0}> <IssueTitleBar - branch={this.props.branch} + branchLike={this.props.branchLike} currentPopup={this.props.currentPopup} displayLocationsCount={this.props.displayLocationsCount} displayLocationsLink={this.props.displayLocationsLink} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js index 7a1cf1d5d67..13de79da203 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -26,6 +26,7 @@ import SimilarIssuesFilter from './SimilarIssuesFilter'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import LocationIndex from '../../common/LocationIndex'; import Tooltip from '../../controls/Tooltip'; +import { getBranchLikeQuery } from '../../../helpers/branches'; import { getComponentIssuesUrl } from '../../../helpers/urls'; import { formatMeasure } from '../../../helpers/measures'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -33,7 +34,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; /*:: type Props = {| - branch?: string, + branchLike?: { id?: string; name: string }, currentPopup: ?string, displayLocationsCount?: boolean; displayLocationsLink?: boolean; @@ -68,7 +69,7 @@ export default function IssueTitleBar(props /*: Props */) { const displayLocations = props.displayLocationsCount && locationsCount > 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( <IssueTitleBar - branch="feature-1.0" + branchLike={{ isMain: false, name: 'feature-1.0', type: 'SHORT' }} issue={issue} currentPopup={null} onFail={jest.fn()} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap index d9f6c77f6b9..30fc9e84c33 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -160,7 +160,6 @@ exports[`should render the titlebar with the filter 1`] = ` Object { "pathname": "/project/issues", "query": Object { - "branch": undefined, "id": "myproject", "issues": "AVsae-CQS-9G3txfbFN2", "open": "AVsae-CQS-9G3txfbFN2", diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts index 61de9d4f695..a903ad15af9 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts @@ -19,10 +19,10 @@ */ import * as React from 'react'; import { History } from '../../api/time-machine'; -import { Metric } from '../../app/types'; +import { Metric, BranchLike } from '../../app/types'; interface Props { - branch?: string; + branchLike?: BranchLike; history?: History; metrics: { [key: string]: Metric }; project: string; diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js index 124b818a267..ff32a9df3c6 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js @@ -34,12 +34,13 @@ import { } from '../../apps/projectActivity/utils'; import { getCustomGraph, getGraph } from '../../helpers/storage'; import { formatMeasure, getShortType } from '../../helpers/measures'; +import { getBranchLikeQuery } from '../../helpers/branches'; /*:: import type { Serie } from '../charts/AdvancedTimeline'; */ /*:: import type { History, Metric } from '../../apps/overview/types'; */ /*:: type Props = { - branch?: string, + branchLike?: { id?: string; name: string }, history: ?History, metrics: { [string]: Metric }, project: string, @@ -144,7 +145,7 @@ export default class PreviewGraph extends React.PureComponent { handleClick = () => { 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<Props> { 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<Props> { const url = getComponentDrilldownUrl( this.props.component, this.props.metric, - this.props.branch + this.props.branchLike ); return ( <Link to={url} className={this.props.className}> 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({ <WithStore> <SourceViewer aroundLine={line} - branch={branch} + branchLike={branchLike} component={key} fromWorkspace={true} highlightedLine={line} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts index 501ff66ed76..abf8bd87438 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts @@ -17,32 +17,39 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { sortBranchesAsTree } from '../branches'; -import { MainBranch, BranchType, ShortLivingBranch, LongLivingBranch } from '../../app/types'; +import { sortBranchesAsTree, isSameBranchLike } from '../branches'; +import { + MainBranch, + BranchType, + ShortLivingBranch, + LongLivingBranch, + PullRequest +} from '../../app/types'; describe('#sortBranchesAsTree', () => { 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>): 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>): 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>): 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 #------------------------------------------------------------------------------ |