@@ -32,7 +32,7 @@ import { | |||
jest.mock('../branches'); | |||
const defaultBranches: Branch[] = [ | |||
mockBranch({ isMain: true, name: 'master', status: { qualityGateStatus: 'OK' } }), | |||
mockBranch({ isMain: true, name: 'main', status: { qualityGateStatus: 'OK' } }), | |||
mockBranch({ | |||
excludedFromPurge: false, | |||
name: 'delete-branch', | |||
@@ -113,10 +113,19 @@ export default class BranchesServiceMock { | |||
this.branches = []; | |||
}; | |||
emptyBranchesAndPullRequest = () => { | |||
this.branches = []; | |||
this.pullRequests = []; | |||
}; | |||
addBranch = (branch: Branch) => { | |||
this.branches.push(branch); | |||
}; | |||
addPullRequest = (branch: PullRequest) => { | |||
this.pullRequests.push(branch); | |||
}; | |||
reset = () => { | |||
this.branches = cloneDeep(defaultBranches); | |||
this.pullRequests = cloneDeep(defaultPullRequests); |
@@ -137,7 +137,7 @@ export default class SecurityHotspotServiceMock { | |||
branch?: string; | |||
} | |||
) => { | |||
if (data?.branch === 'b1') { | |||
if (data?.branch === 'normal-branch') { | |||
return this.reply({ | |||
paging: mockPaging(), | |||
hotspots: [ | |||
@@ -198,7 +198,7 @@ export default class SecurityHotspotServiceMock { | |||
inNewCodePeriod?: boolean; | |||
branch?: string; | |||
}) => { | |||
if (data?.branch === 'b1') { | |||
if (data?.branch === 'normal-branch') { | |||
return this.reply({ | |||
paging: mockPaging({ pageIndex: 1, pageSize: data.ps, total: 2 }), | |||
hotspots: [ |
@@ -22,17 +22,10 @@ import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { Outlet } from 'react-router-dom'; | |||
import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings'; | |||
import { getBranches, getPullRequests } from '../../api/branches'; | |||
import { getAnalysisStatus, getTasksForComponent } from '../../api/ce'; | |||
import { getTasksForComponent } from '../../api/ce'; | |||
import { getComponentData } from '../../api/components'; | |||
import { getComponentNavigation } from '../../api/navigation'; | |||
import { Location, Router, withRouter } from '../../components/hoc/withRouter'; | |||
import { | |||
getBranchLikeQuery, | |||
isBranch, | |||
isMainBranch, | |||
isPullRequest, | |||
} from '../../helpers/branch-like'; | |||
import { translateWithParameters } from '../../helpers/l10n'; | |||
import { HttpStatus } from '../../helpers/request'; | |||
import { getPortfolioUrl } from '../../helpers/urls'; | |||
@@ -40,30 +33,25 @@ import { | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingResponse, | |||
} from '../../types/alm-settings'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | |||
import { Feature } from '../../types/features'; | |||
import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks'; | |||
import { Component, Status } from '../../types/types'; | |||
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks'; | |||
import { Component } from '../../types/types'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
import ComponentContainerNotFound from './ComponentContainerNotFound'; | |||
import withAvailableFeatures, { | |||
WithAvailableFeaturesProps, | |||
} from './available-features/withAvailableFeatures'; | |||
import withBranchStatusActions from './branch-status/withBranchStatusActions'; | |||
import { ComponentContext } from './componentContext/ComponentContext'; | |||
import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation'; | |||
import ComponentNav from './nav/component/ComponentNav'; | |||
interface Props extends WithAvailableFeaturesProps { | |||
location: Location; | |||
updateBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; | |||
router: Router; | |||
} | |||
interface State { | |||
branchLike?: BranchLike; | |||
branchLikes: BranchLike[]; | |||
component?: Component; | |||
currentTask?: Task; | |||
isPending: boolean; | |||
@@ -71,7 +59,6 @@ interface State { | |||
projectBinding?: ProjectAlmBindingResponse; | |||
projectBindingErrors?: ProjectAlmBindingConfigurationErrors; | |||
tasksInProgress?: Task[]; | |||
warnings: TaskWarning[]; | |||
} | |||
const FETCH_STATUS_WAIT_TIME = 3000; | |||
@@ -79,7 +66,7 @@ const FETCH_STATUS_WAIT_TIME = 3000; | |||
export class ComponentContainer extends React.PureComponent<Props, State> { | |||
watchStatusTimer?: number; | |||
mounted = false; | |||
state: State = { branchLikes: [], isPending: false, loading: true, warnings: [] }; | |||
state: State = { isPending: false, loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -135,8 +122,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
this.props.router.replace(getPortfolioUrl(componentWithQualifier.key)); | |||
} | |||
const { branchLike, branchLikes } = await this.fetchBranches(componentWithQualifier); | |||
let projectBinding; | |||
if (componentWithQualifier.qualifier === ComponentQualifier.Project) { | |||
projectBinding = await getProjectAlmBinding(key).catch(() => undefined); | |||
@@ -144,59 +129,25 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
if (this.mounted) { | |||
this.setState({ | |||
branchLike, | |||
branchLikes, | |||
component: componentWithQualifier, | |||
projectBinding, | |||
loading: false, | |||
}); | |||
this.fetchStatus(componentWithQualifier.key); | |||
this.fetchWarnings(componentWithQualifier, branchLike); | |||
this.fetchProjectBindingErrors(componentWithQualifier); | |||
} | |||
}; | |||
fetchBranches = async (componentWithQualifier: Component) => { | |||
const { hasFeature } = this.props; | |||
const breadcrumb = componentWithQualifier.breadcrumbs.find(({ qualifier }) => { | |||
return ([ComponentQualifier.Application, ComponentQualifier.Project] as string[]).includes( | |||
qualifier | |||
); | |||
}); | |||
let branchLike = undefined; | |||
let branchLikes: BranchLike[] = []; | |||
if (breadcrumb) { | |||
const { key } = breadcrumb; | |||
const [branches, pullRequests] = await Promise.all([ | |||
getBranches(key), | |||
!hasFeature(Feature.BranchSupport) || | |||
breadcrumb.qualifier === ComponentQualifier.Application | |||
? Promise.resolve([]) | |||
: getPullRequests(key), | |||
]); | |||
branchLikes = [...branches, ...pullRequests]; | |||
branchLike = this.getCurrentBranchLike(branchLikes); | |||
this.registerBranchStatuses(branchLikes, componentWithQualifier); | |||
} | |||
return { branchLike, branchLikes }; | |||
}; | |||
fetchStatus = (componentKey: string) => { | |||
getTasksForComponent(componentKey).then( | |||
({ current, queue }) => { | |||
if (this.mounted) { | |||
let shouldFetchComponent = false; | |||
this.setState( | |||
({ branchLike, component, currentTask, tasksInProgress }) => { | |||
const newCurrentTask = this.getCurrentTask(current, branchLike); | |||
const pendingTasks = this.getPendingTasksForBranchLike(queue, branchLike); | |||
({ component, currentTask, tasksInProgress }) => { | |||
const newCurrentTask = this.getCurrentTask(current); | |||
const pendingTasks = this.getPendingTasksForBranchLike(queue); | |||
const newTasksInProgress = this.getInProgressTasks(pendingTasks); | |||
shouldFetchComponent = this.computeShouldFetchComponent( | |||
@@ -235,20 +186,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
); | |||
}; | |||
fetchWarnings = (component: Component, branchLike?: BranchLike) => { | |||
if (component.qualifier === ComponentQualifier.Project) { | |||
getAnalysisStatus({ | |||
component: component.key, | |||
...getBranchLikeQuery(branchLike), | |||
}).then( | |||
({ component }) => { | |||
this.setState({ warnings: component.warnings }); | |||
}, | |||
() => {} | |||
); | |||
} | |||
}; | |||
fetchProjectBindingErrors = async (component: Component) => { | |||
if ( | |||
component.qualifier === ComponentQualifier.Project && | |||
@@ -269,27 +206,18 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier, | |||
}); | |||
getCurrentBranchLike = (branchLikes: BranchLike[]) => { | |||
const { query } = this.props.location; | |||
return query.pullRequest | |||
? branchLikes.find((b) => isPullRequest(b) && b.key === query.pullRequest) | |||
: branchLikes.find((b) => isBranch(b) && (query.branch ? b.name === query.branch : b.isMain)); | |||
}; | |||
getCurrentTask = (current: Task, branchLike?: BranchLike) => { | |||
getCurrentTask = (current: Task) => { | |||
if (!current || !this.isReportRelatedTask(current)) { | |||
return undefined; | |||
} | |||
return current.status === TaskStatuses.Failed || this.isSameBranch(current, branchLike) | |||
return current.status === TaskStatuses.Failed || this.isSameBranch(current) | |||
? current | |||
: undefined; | |||
}; | |||
getPendingTasksForBranchLike = (pendingTasks: Task[], branchLike?: BranchLike) => { | |||
return pendingTasks.filter( | |||
(task) => this.isReportRelatedTask(task) && this.isSameBranch(task, branchLike) | |||
); | |||
getPendingTasksForBranchLike = (pendingTasks: Task[]) => { | |||
return pendingTasks.filter((task) => this.isReportRelatedTask(task) && this.isSameBranch(task)); | |||
}; | |||
getInProgressTasks = (pendingTasks: Task[]) => { | |||
@@ -346,31 +274,19 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
); | |||
}; | |||
isSameBranch = (task: Pick<Task, 'branch' | 'pullRequest'>, branchLike?: BranchLike) => { | |||
if (branchLike) { | |||
if (isMainBranch(branchLike)) { | |||
return (!task.pullRequest && !task.branch) || branchLike.name === task.branch; | |||
} | |||
if (isPullRequest(branchLike)) { | |||
return branchLike.key === task.pullRequest; | |||
} | |||
if (isBranch(branchLike)) { | |||
return branchLike.name === task.branch; | |||
} | |||
} | |||
return !task.branch && !task.pullRequest; | |||
}; | |||
isSameBranch = (task: Pick<Task, 'branch' | 'pullRequest'>) => { | |||
const { branch, pullRequest } = this.props.location.query; | |||
registerBranchStatuses = (branchLikes: BranchLike[], component: Component) => { | |||
branchLikes.forEach((branchLike) => { | |||
if (branchLike.status) { | |||
this.props.updateBranchStatus( | |||
branchLike, | |||
component.key, | |||
branchLike.status.qualityGateStatus | |||
); | |||
} | |||
}); | |||
if (!pullRequest && !branch) { | |||
return !task.branch && !task.pullRequest; | |||
} | |||
if (pullRequest) { | |||
return pullRequest === task.pullRequest; | |||
} | |||
if (branch) { | |||
return branch === task.branch; | |||
} | |||
return false; | |||
}; | |||
handleComponentChange = (changes: Partial<Component>) => { | |||
@@ -385,33 +301,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
handleBranchesChange = () => { | |||
const { router, location } = this.props; | |||
const { component } = this.state; | |||
if (this.mounted && component) { | |||
this.fetchBranches(component).then( | |||
({ branchLike, branchLikes }) => { | |||
if (this.mounted) { | |||
this.setState({ branchLike, branchLikes }); | |||
if (branchLike === undefined) { | |||
router.replace({ query: { ...location.query, branch: undefined } }); | |||
} | |||
} | |||
}, | |||
() => {} | |||
); | |||
} | |||
}; | |||
handleWarningDismiss = () => { | |||
const { component } = this.state; | |||
if (component !== undefined) { | |||
this.fetchWarnings(component); | |||
} | |||
}; | |||
render() { | |||
const { component, loading } = this.state; | |||
@@ -423,16 +312,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
return <PageUnavailableDueToIndexation component={component} />; | |||
} | |||
const { | |||
branchLike, | |||
branchLikes, | |||
currentTask, | |||
isPending, | |||
projectBinding, | |||
projectBindingErrors, | |||
tasksInProgress, | |||
warnings, | |||
} = this.state; | |||
const { currentTask, isPending, projectBinding, projectBindingErrors, tasksInProgress } = | |||
this.state; | |||
const isInProgress = tasksInProgress && tasksInProgress.length > 0; | |||
return ( | |||
@@ -449,17 +330,12 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
component.qualifier | |||
) && ( | |||
<ComponentNav | |||
branchLikes={branchLikes} | |||
component={component} | |||
currentBranchLike={branchLike} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
onWarningDismiss={this.handleWarningDismiss} | |||
projectBinding={projectBinding} | |||
projectBindingErrors={projectBindingErrors} | |||
warnings={warnings} | |||
/> | |||
)} | |||
{loading ? ( | |||
@@ -469,12 +345,9 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
) : ( | |||
<ComponentContext.Provider | |||
value={{ | |||
branchLike, | |||
branchLikes, | |||
component, | |||
isInProgress, | |||
isPending, | |||
onBranchesChange: this.handleBranchesChange, | |||
onComponentChange: this.handleComponentChange, | |||
projectBinding, | |||
}} | |||
@@ -487,4 +360,4 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
export default withRouter(withAvailableFeatures(withBranchStatusActions(ComponentContainer))); | |||
export default withRouter(withAvailableFeatures(ComponentContainer)); |
@@ -29,7 +29,6 @@ import Workspace from '../../components/workspace/Workspace'; | |||
import GlobalFooter from './GlobalFooter'; | |||
import StartupModal from './StartupModal'; | |||
import SystemAnnouncement from './SystemAnnouncement'; | |||
import BranchStatusContextProvider from './branch-status/BranchStatusContextProvider'; | |||
import IndexationContextProvider from './indexation/IndexationContextProvider'; | |||
import IndexationNotification from './indexation/IndexationNotification'; | |||
import LanguagesContextProvider from './languages/LanguagesContextProvider'; | |||
@@ -68,21 +67,19 @@ export default function GlobalContainer() { | |||
id="container" | |||
> | |||
<div className="page-container"> | |||
<BranchStatusContextProvider> | |||
<Workspace> | |||
<IndexationContextProvider> | |||
<LanguagesContextProvider> | |||
<MetricsContextProvider> | |||
<SystemAnnouncement /> | |||
<IndexationNotification /> | |||
<UpdateNotification dismissable /> | |||
<GlobalNav location={location} /> | |||
<Outlet /> | |||
</MetricsContextProvider> | |||
</LanguagesContextProvider> | |||
</IndexationContextProvider> | |||
</Workspace> | |||
</BranchStatusContextProvider> | |||
<Workspace> | |||
<IndexationContextProvider> | |||
<LanguagesContextProvider> | |||
<MetricsContextProvider> | |||
<SystemAnnouncement /> | |||
<IndexationNotification /> | |||
<UpdateNotification dismissable /> | |||
<GlobalNav location={location} /> | |||
<Outlet /> | |||
</MetricsContextProvider> | |||
</LanguagesContextProvider> | |||
</IndexationContextProvider> | |||
</Workspace> | |||
</div> | |||
<PromotionNotification /> | |||
</div> |
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getProjectAlmBinding, validateProjectAlmBinding } from '../../../api/alm-settings'; | |||
import { getBranches, getPullRequests } from '../../../api/branches'; | |||
import { getAnalysisStatus, getTasksForComponent } from '../../../api/ce'; | |||
import { getTasksForComponent } from '../../../api/ce'; | |||
import { getComponentData } from '../../../api/components'; | |||
import { getComponentNavigation } from '../../../api/navigation'; | |||
import { mockProjectAlmBindingConfigurationErrors } from '../../../helpers/mocks/alm-settings'; | |||
@@ -97,7 +97,6 @@ afterEach(() => { | |||
it('changes component', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ | |||
branchLikes: [mockMainBranch()], | |||
component: { | |||
qualifier: ComponentQualifier.Project, | |||
visibility: Visibility.Public, | |||
@@ -147,42 +146,6 @@ it("doesn't load branches portfolio", async () => { | |||
}); | |||
}); | |||
it('updates branches on change', async () => { | |||
const updateBranchStatus = jest.fn(); | |||
const wrapper = shallowRender({ | |||
hasFeature: () => true, | |||
location: mockLocation({ query: { id: 'portfolioKey' } }), | |||
updateBranchStatus, | |||
}); | |||
wrapper.setState({ | |||
branchLikes: [mockMainBranch()], | |||
component: mockComponent({ | |||
breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: ComponentQualifier.Project }], | |||
}), | |||
loading: false, | |||
}); | |||
wrapper.instance().handleBranchesChange(); | |||
expect(getBranches).toHaveBeenCalledWith('projectKey'); | |||
expect(getPullRequests).toHaveBeenCalledWith('projectKey'); | |||
await waitAndUpdate(wrapper); | |||
expect(updateBranchStatus).toHaveBeenCalledTimes(2); | |||
}); | |||
it('sets main branch when current branch is not found', async () => { | |||
const router = mockRouter(); | |||
const wrapper = shallowRender({ | |||
hasFeature: () => true, | |||
location: mockLocation({ query: { id: 'portfolioKey', branch: 'any-branch' } }), | |||
router, | |||
}); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleBranchesChange(); | |||
await waitAndUpdate(wrapper); | |||
expect(router.replace).toHaveBeenCalledWith({ query: { id: 'portfolioKey' } }); | |||
}); | |||
it('fetches status', async () => { | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ | |||
component: {}, | |||
@@ -198,31 +161,29 @@ it('filters correctly the pending tasks for a main branch', () => { | |||
const component = wrapper.instance(); | |||
const mainBranch = mockMainBranch(); | |||
const branch3 = mockBranch({ name: 'branch-3' }); | |||
const branch2 = mockBranch({ name: 'branch-2' }); | |||
const pullRequest = mockPullRequest(); | |||
expect(component.isSameBranch({})).toBe(true); | |||
expect(component.isSameBranch({}, mainBranch)).toBe(true); | |||
expect(component.isSameBranch({ branch: mainBranch.name }, mainBranch)).toBe(true); | |||
expect(component.isSameBranch({}, branch3)).toBe(false); | |||
expect(component.isSameBranch({ branch: branch3.name }, branch3)).toBe(true); | |||
expect(component.isSameBranch({ branch: 'feature' }, branch2)).toBe(false); | |||
expect(component.isSameBranch({ branch: 'branch-6.6' }, branch2)).toBe(false); | |||
expect(component.isSameBranch({ branch: branch2.name }, branch2)).toBe(true); | |||
expect(component.isSameBranch({ branch: 'branch-6.7' }, pullRequest)).toBe(false); | |||
expect(component.isSameBranch({ pullRequest: pullRequest.key }, pullRequest)).toBe(true); | |||
wrapper.setProps({ location: mockLocation({ query: { branch: mainBranch.name } }) }); | |||
expect(component.isSameBranch({ branch: mainBranch.name })).toBe(true); | |||
expect(component.isSameBranch({})).toBe(false); | |||
wrapper.setProps({ location: mockLocation({ query: { branch: branch3.name } }) }); | |||
expect(component.isSameBranch({ branch: branch3.name })).toBe(true); | |||
wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) }); | |||
expect(component.isSameBranch({ pullRequest: pullRequest.key })).toBe(true); | |||
const currentTask = mockTask({ pullRequest: pullRequest.key, status: TaskStatuses.InProgress }); | |||
const failedTask = { ...currentTask, status: TaskStatuses.Failed }; | |||
const pendingTasks = [currentTask, mockTask({ branch: branch3.name }), mockTask()]; | |||
expect(component.getCurrentTask(failedTask)).toBe(failedTask); | |||
wrapper.setProps({ location: mockLocation({ query: {} }) }); | |||
expect(component.getCurrentTask(currentTask)).toBeUndefined(); | |||
expect(component.getCurrentTask(failedTask, mainBranch)).toBe(failedTask); | |||
expect(component.getCurrentTask(currentTask, mainBranch)).toBeUndefined(); | |||
expect(component.getCurrentTask(currentTask, pullRequest)).toMatchObject(currentTask); | |||
expect(component.getPendingTasksForBranchLike(pendingTasks, mainBranch)).toMatchObject([{}]); | |||
expect(component.getPendingTasksForBranchLike(pendingTasks, pullRequest)).toMatchObject([ | |||
currentTask, | |||
]); | |||
wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) }); | |||
expect(component.getCurrentTask(currentTask)).toMatchObject(currentTask); | |||
expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([currentTask]); | |||
wrapper.setProps({ location: mockLocation({ query: {} }) }); | |||
expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([{}]); | |||
}); | |||
it('reload component after task progress finished', async () => { | |||
@@ -393,22 +354,6 @@ it('should display display the unavailable page if the component needs issue syn | |||
expect(wrapper.find(PageUnavailableDueToIndexation).exists()).toBe(true); | |||
}); | |||
it('should correctly reload last task warnings if anything got dismissed', async () => { | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ | |||
component: mockComponent({ | |||
breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }], | |||
}), | |||
}); | |||
(getComponentNavigation as jest.Mock).mockResolvedValueOnce({}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
(getAnalysisStatus as jest.Mock).mockClear(); | |||
wrapper.instance().handleWarningDismiss(); | |||
expect(getAnalysisStatus).toHaveBeenCalledTimes(1); | |||
}); | |||
describe('should correctly validate the project binding depending on the context', () => { | |||
const COMPONENT = mockComponent({ | |||
breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }], | |||
@@ -461,7 +406,6 @@ function shallowRender(props: Partial<ComponentContainer['props']> = {}) { | |||
<ComponentContainer | |||
hasFeature={jest.fn().mockReturnValue(false)} | |||
location={mockLocation({ query: { id: 'foo' } })} | |||
updateBranchStatus={jest.fn()} | |||
router={mockRouter()} | |||
{...props} | |||
> |
@@ -1,104 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { getQualityGateProjectStatus } from '../../../api/quality-gates'; | |||
import { getBranchLikeKey, getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { extractStatusConditionsFromProjectStatus } from '../../../helpers/qualityGates'; | |||
import { BranchLike, BranchStatusData } from '../../../types/branch-like'; | |||
import { QualityGateStatusCondition } from '../../../types/quality-gates'; | |||
import { Dict, Status } from '../../../types/types'; | |||
import { BranchStatusContext } from './BranchStatusContext'; | |||
interface State { | |||
branchStatusByComponent: Dict<Dict<BranchStatusData>>; | |||
} | |||
export default class BranchStatusContextProvider extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
state: State = { | |||
branchStatusByComponent: {}, | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchBranchStatus = async (branchLike: BranchLike, projectKey: string) => { | |||
const projectStatus = await getQualityGateProjectStatus({ | |||
projectKey, | |||
...getBranchLikeQuery(branchLike), | |||
}).catch(() => undefined); | |||
if (!this.mounted || projectStatus === undefined) { | |||
return; | |||
} | |||
const { ignoredConditions, status } = projectStatus; | |||
const conditions = extractStatusConditionsFromProjectStatus(projectStatus); | |||
this.updateBranchStatus(branchLike, projectKey, status, conditions, ignoredConditions); | |||
}; | |||
updateBranchStatus = ( | |||
branchLike: BranchLike, | |||
projectKey: string, | |||
status: Status, | |||
conditions?: QualityGateStatusCondition[], | |||
ignoredConditions?: boolean | |||
) => { | |||
const branchLikeKey = getBranchLikeKey(branchLike); | |||
this.setState(({ branchStatusByComponent }) => ({ | |||
branchStatusByComponent: { | |||
...branchStatusByComponent, | |||
[projectKey]: { | |||
...(branchStatusByComponent[projectKey] || {}), | |||
[branchLikeKey]: { | |||
conditions, | |||
ignoredConditions, | |||
status, | |||
}, | |||
}, | |||
}, | |||
})); | |||
}; | |||
render() { | |||
return ( | |||
<BranchStatusContext.Provider | |||
value={{ | |||
branchStatusByComponent: this.state.branchStatusByComponent, | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => { | |||
this.fetchBranchStatus(branchLike, projectKey).catch(() => { | |||
/* noop */ | |||
}); | |||
}, | |||
updateBranchStatus: this.updateBranchStatus, | |||
}} | |||
> | |||
{this.props.children} | |||
</BranchStatusContext.Provider> | |||
); | |||
} | |||
} |
@@ -1,73 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getQualityGateProjectStatus } from '../../../../api/quality-gates'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { BranchStatusData } from '../../../../types/branch-like'; | |||
import BranchStatusContextProvider from '../BranchStatusContextProvider'; | |||
jest.mock('../../../../api/quality-gates', () => ({ | |||
getQualityGateProjectStatus: jest.fn().mockResolvedValue({}), | |||
})); | |||
describe('fetchBranchStatus', () => { | |||
it('should get the branch status', async () => { | |||
const projectKey = 'projectKey'; | |||
const branchName = 'branch-6.7'; | |||
const status: BranchStatusData = { | |||
status: 'OK', | |||
conditions: [], | |||
ignoredConditions: false, | |||
}; | |||
(getQualityGateProjectStatus as jest.Mock).mockResolvedValueOnce(status); | |||
const wrapper = shallowRender(); | |||
wrapper.instance().fetchBranchStatus(mockBranch({ name: branchName }), projectKey); | |||
expect(getQualityGateProjectStatus).toHaveBeenCalledWith({ projectKey, branch: branchName }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().branchStatusByComponent).toEqual({ | |||
[projectKey]: { [`branch-${branchName}`]: status }, | |||
}); | |||
}); | |||
it('should ignore errors', async () => { | |||
(getQualityGateProjectStatus as jest.Mock).mockRejectedValueOnce('error'); | |||
const wrapper = shallowRender(); | |||
wrapper.instance().fetchBranchStatus(mockBranch(), 'project'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().branchStatusByComponent).toEqual({}); | |||
}); | |||
}); | |||
function shallowRender() { | |||
return shallow<BranchStatusContextProvider>( | |||
<BranchStatusContextProvider> | |||
<div /> | |||
</BranchStatusContextProvider> | |||
); | |||
} |
@@ -1,58 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { getWrappedDisplayName } from '../../../components/hoc/utils'; | |||
import { getBranchStatusByBranchLike } from '../../../helpers/branch-like'; | |||
import { BranchLike, BranchStatusData } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
import { BranchStatusContext } from './BranchStatusContext'; | |||
export default function withBranchStatus< | |||
P extends { branchLike: BranchLike; component: Component } | |||
>(WrappedComponent: React.ComponentType<P & BranchStatusData>) { | |||
return class WithBranchStatus extends React.PureComponent<Omit<P, keyof BranchStatusData>> { | |||
static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatus'); | |||
render() { | |||
const { branchLike, component } = this.props; | |||
return ( | |||
<BranchStatusContext.Consumer> | |||
{({ branchStatusByComponent }) => { | |||
const { conditions, ignoredConditions, status } = getBranchStatusByBranchLike( | |||
branchStatusByComponent, | |||
component.key, | |||
branchLike | |||
); | |||
return ( | |||
<WrappedComponent | |||
conditions={conditions} | |||
ignoredConditions={ignoredConditions} | |||
status={status} | |||
{...(this.props as P)} | |||
/> | |||
); | |||
}} | |||
</BranchStatusContext.Consumer> | |||
); | |||
} | |||
}; | |||
} |
@@ -1,50 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { getWrappedDisplayName } from '../../../components/hoc/utils'; | |||
import { BranchStatusContext, BranchStatusContextInterface } from './BranchStatusContext'; | |||
export type WithBranchStatusActionsProps = | |||
| Pick<BranchStatusContextInterface, 'fetchBranchStatus'> | |||
| Pick<BranchStatusContextInterface, 'updateBranchStatus'>; | |||
export default function withBranchStatusActions<P>( | |||
WrappedComponent: React.ComponentType<P & WithBranchStatusActionsProps> | |||
) { | |||
return class WithBranchStatusActions extends React.PureComponent< | |||
Omit<P, keyof BranchStatusContextInterface> | |||
> { | |||
static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatusActions'); | |||
render() { | |||
return ( | |||
<BranchStatusContext.Consumer> | |||
{({ fetchBranchStatus, updateBranchStatus }) => ( | |||
<WrappedComponent | |||
fetchBranchStatus={fetchBranchStatus} | |||
updateBranchStatus={updateBranchStatus} | |||
{...(this.props as P)} | |||
/> | |||
)} | |||
</BranchStatusContext.Consumer> | |||
); | |||
} | |||
}; | |||
} |
@@ -22,7 +22,5 @@ import * as React from 'react'; | |||
import { ComponentContextShape } from '../../../types/component'; | |||
export const ComponentContext = React.createContext<ComponentContextShape>({ | |||
branchLikes: [], | |||
onBranchesChange: noop, | |||
onComponentChange: noop, | |||
}); |
@@ -25,7 +25,7 @@ export default function withCurrentUserContext<P>( | |||
WrappedComponent: React.ComponentType<P & Pick<CurrentUserContextInterface, 'currentUser'>> | |||
) { | |||
return class WithCurrentUserContext extends React.PureComponent< | |||
Omit<P, keyof CurrentUserContextInterface> | |||
Omit<P, 'currentUser' | 'updateCurrentUserHomepage' | 'updateDismissedNotices'> | |||
> { | |||
static displayName = getWrappedDisplayName(WrappedComponent, 'withCurrentUserContext'); | |||
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { withTheme } from '@emotion/react'; | |||
import { QueryClient } from '@tanstack/react-query'; | |||
import { Theme } from 'design-system'; | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
@@ -28,6 +29,7 @@ import { addGlobalErrorMessage } from '../../../helpers/globalMessages'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getCurrentL10nBundle } from '../../../helpers/l10nBundle'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { withQueryClient } from '../../../queries/withQueryClientHoc'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { ExtensionStartMethod } from '../../../types/extension'; | |||
import { Dict, Extension as TypeExtension } from '../../../types/types'; | |||
@@ -44,6 +46,7 @@ export interface ExtensionProps extends WrappedComponentProps { | |||
location: Location; | |||
options?: Dict<any>; | |||
router: Router; | |||
queryClient: QueryClient; | |||
updateCurrentUserHomepage: (homepage: HomePage) => void; | |||
} | |||
@@ -74,7 +77,7 @@ class Extension extends React.PureComponent<ExtensionProps, State> { | |||
} | |||
handleStart = (start: ExtensionStartMethod) => { | |||
const { theme: dsTheme } = this.props; | |||
const { theme: dsTheme, queryClient } = this.props; | |||
const result = start({ | |||
appState: this.props.appState, | |||
el: this.container, | |||
@@ -90,6 +93,7 @@ class Extension extends React.PureComponent<ExtensionProps, State> { | |||
// See SONAR-16207 and core-extension-enterprise-server/src/main/js/portfolios/components/Header.tsx | |||
// for more information on why we're passing this as a prop to an extension. | |||
updateCurrentUserHomepage: this.props.updateCurrentUserHomepage, | |||
queryClient, | |||
...this.props.options, | |||
}); | |||
@@ -134,5 +138,5 @@ class Extension extends React.PureComponent<ExtensionProps, State> { | |||
} | |||
export default injectIntl( | |||
withRouter(withTheme(withAppStateContext(withCurrentUserContext(Extension)))) | |||
withRouter(withTheme(withAppStateContext(withCurrentUserContext(withQueryClient(Extension))))) | |||
); |
@@ -19,13 +19,17 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { useParams } from 'react-router-dom'; | |||
import { useRefreshBranches } from '../../../queries/branch'; | |||
import NotFound from '../NotFound'; | |||
import { ComponentContext } from '../componentContext/ComponentContext'; | |||
import Extension from './Extension'; | |||
export default function ProjectAdminPageExtension() { | |||
const { extensionKey, pluginKey } = useParams(); | |||
const { component, onBranchesChange, onComponentChange } = React.useContext(ComponentContext); | |||
const { component, onComponentChange } = React.useContext(ComponentContext); | |||
// We keep that for compatibility but ideally should advocate to use tanstack query | |||
const onBranchesChange = useRefreshBranches(); | |||
const extension = | |||
component && | |||
@@ -35,7 +39,7 @@ export default function ProjectAdminPageExtension() { | |||
); | |||
return extension ? ( | |||
<Extension extension={extension} options={{ component, onBranchesChange, onComponentChange }} /> | |||
<Extension extension={extension} options={{ component, onComponentChange, onBranchesChange }} /> | |||
) : ( | |||
<NotFound withContainer={false} /> | |||
); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { useParams } from 'react-router-dom'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
import NotFound from '../NotFound'; | |||
import { ComponentContext } from '../componentContext/ComponentContext'; | |||
import Extension from './Extension'; | |||
@@ -32,12 +33,14 @@ export interface ProjectPageExtensionProps { | |||
export default function ProjectPageExtension({ params }: ProjectPageExtensionProps) { | |||
const { extensionKey, pluginKey } = useParams(); | |||
const { branchLike, component } = React.useContext(ComponentContext); | |||
const { component } = React.useContext(ComponentContext); | |||
const { data } = useBranchesQuery(component); | |||
if (component === undefined) { | |||
if (component === undefined || data === undefined) { | |||
return null; | |||
} | |||
const { branchLike } = data; | |||
const fullKey = | |||
params !== undefined | |||
? `${params.pluginKey}/${params.extensionKey}` |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | |||
import { render, screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { HelmetProvider } from 'react-helmet-async'; | |||
@@ -59,17 +60,20 @@ function renderProjectAdminPageExtension( | |||
} | |||
) { | |||
const { pluginKey, extensionKey } = params; | |||
const queryClient = new QueryClient(); | |||
return render( | |||
<HelmetProvider context={{}}> | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<ComponentContext.Provider value={{ component } as ComponentContextShape}> | |||
<MemoryRouter initialEntries={[`/${pluginKey}/${extensionKey}`]}> | |||
<Routes> | |||
<Route path="/:pluginKey/:extensionKey" element={<ProjectAdminPageExtension />} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</ComponentContext.Provider> | |||
</IntlProvider> | |||
</HelmetProvider> | |||
<QueryClientProvider client={queryClient}> | |||
<HelmetProvider context={{}}> | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<ComponentContext.Provider value={{ component } as ComponentContextShape}> | |||
<MemoryRouter initialEntries={[`/${pluginKey}/${extensionKey}`]}> | |||
<Routes> | |||
<Route path="/:pluginKey/:extensionKey" element={<ProjectAdminPageExtension />} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</ComponentContext.Provider> | |||
</IntlProvider> | |||
</HelmetProvider> | |||
</QueryClientProvider> | |||
); | |||
} |
@@ -17,11 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { render, screen } from '@testing-library/react'; | |||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | |||
import { render, screen, waitFor } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { HelmetProvider } from 'react-helmet-async'; | |||
import { IntlProvider } from 'react-intl'; | |||
import { MemoryRouter, Route, Routes } from 'react-router-dom'; | |||
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; | |||
import { getExtensionStart } from '../../../../helpers/extensions'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { ComponentContextShape } from '../../../../types/component'; | |||
@@ -33,51 +35,60 @@ jest.mock('../../../../helpers/extensions', () => ({ | |||
getExtensionStart: jest.fn().mockResolvedValue(jest.fn()), | |||
})); | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => { | |||
handler.reset(); | |||
}); | |||
it('should not render when no component is passed', () => { | |||
renderProjectPageExtension(); | |||
expect(screen.queryByText('page_not_found')).not.toBeInTheDocument(); | |||
expect(getExtensionStart).not.toHaveBeenCalledWith('pluginId/extensionId'); | |||
}); | |||
it('should render correctly when the extension is found', () => { | |||
it('should render correctly when the extension is found', async () => { | |||
renderProjectPageExtension( | |||
mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }), | |||
{ params: { pluginKey: 'pluginId', extensionKey: 'extensionId' } } | |||
); | |||
expect(getExtensionStart).toHaveBeenCalledWith('pluginId/extensionId'); | |||
await waitFor(() => expect(getExtensionStart).toHaveBeenCalledWith('pluginId/extensionId')); | |||
}); | |||
it('should render correctly when the extension is not found', () => { | |||
it('should render correctly when the extension is not found', async () => { | |||
renderProjectPageExtension( | |||
mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }), | |||
{ params: { pluginKey: 'not-found-plugin', extensionKey: 'not-found-extension' } } | |||
); | |||
expect(screen.getByText('page_not_found')).toBeInTheDocument(); | |||
expect(await screen.findByText('page_not_found')).toBeInTheDocument(); | |||
}); | |||
function renderProjectPageExtension( | |||
component?: Component, | |||
props?: Partial<ProjectPageExtensionProps> | |||
) { | |||
const queryClient = new QueryClient(); | |||
return render( | |||
<HelmetProvider context={{}}> | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<ComponentContext.Provider value={{ component } as ComponentContextShape}> | |||
<MemoryRouter> | |||
<Routes> | |||
<Route | |||
path="*" | |||
element={ | |||
<ProjectPageExtension | |||
params={{ extensionKey: 'extensionId', pluginKey: 'pluginId' }} | |||
{...props} | |||
/> | |||
} | |||
/> | |||
</Routes> | |||
</MemoryRouter> | |||
</ComponentContext.Provider> | |||
</IntlProvider> | |||
</HelmetProvider> | |||
<QueryClientProvider client={queryClient}> | |||
<HelmetProvider context={{}}> | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<ComponentContext.Provider value={{ component } as ComponentContextShape}> | |||
<MemoryRouter initialEntries={[`/?id=${component?.key}`]}> | |||
<Routes> | |||
<Route | |||
path="*" | |||
element={ | |||
<ProjectPageExtension | |||
params={{ extensionKey: 'extensionId', pluginKey: 'pluginId' }} | |||
{...props} | |||
/> | |||
} | |||
/> | |||
</Routes> | |||
</MemoryRouter> | |||
</ComponentContext.Provider> | |||
</IntlProvider> | |||
</HelmetProvider> | |||
</QueryClientProvider> | |||
); | |||
} |
@@ -21,7 +21,6 @@ import { sortBy } from 'lodash'; | |||
import { ComponentQualifier } from '../../../../js/types/component'; | |||
const ORDER = [ | |||
ComponentQualifier.Developper, | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application, |
@@ -22,20 +22,39 @@ import { Link } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { useLocation } from 'react-router-dom'; | |||
import { isBranch, isMainBranch, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { hasMessage, translate } from '../../../../helpers/l10n'; | |||
import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls'; | |||
import { useBranchesQuery } from '../../../../queries/branch'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
interface Props { | |||
component: Component; | |||
currentTask: Task; | |||
currentTaskOnSameBranch?: boolean; | |||
onLeave: () => void; | |||
} | |||
function isSameBranch(task: Task, branchLike?: BranchLike) { | |||
if (branchLike) { | |||
if (isMainBranch(branchLike)) { | |||
return (!task.pullRequest && !task.branch) || branchLike.name === task.branch; | |||
} | |||
if (isPullRequest(branchLike)) { | |||
return branchLike.key === task.pullRequest; | |||
} | |||
if (isBranch(branchLike)) { | |||
return branchLike.name === task.branch; | |||
} | |||
} | |||
return !task.branch && !task.pullRequest; | |||
} | |||
export function AnalysisErrorMessage(props: Props) { | |||
const { component, currentTask, currentTaskOnSameBranch } = props; | |||
const { component, currentTask } = props; | |||
const { data: { branchLike } = {} } = useBranchesQuery(component); | |||
const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike); | |||
const location = useLocation(); | |||
@@ -29,12 +29,11 @@ import { AnalysisLicenseError } from './AnalysisLicenseError'; | |||
interface Props { | |||
component: Component; | |||
currentTask: Task; | |||
currentTaskOnSameBranch?: boolean; | |||
onClose: () => void; | |||
} | |||
export function AnalysisErrorModal(props: Props) { | |||
const { component, currentTask, currentTaskOnSameBranch } = props; | |||
const { component, currentTask } = props; | |||
const header = translate('error'); | |||
@@ -55,7 +54,6 @@ export function AnalysisErrorModal(props: Props) { | |||
<AnalysisErrorMessage | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTaskOnSameBranch} | |||
onLeave={props.onClose} | |||
/> | |||
)} |
@@ -19,25 +19,23 @@ | |||
*/ | |||
import { DeferredSpinner, FlagMessage, Link } from 'design-system'; | |||
import * as React from 'react'; | |||
import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks'; | |||
import { useBranchWarrningQuery } from '../../../../queries/branch'; | |||
import { Task, TaskStatuses } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { AnalysisErrorModal } from './AnalysisErrorModal'; | |||
import AnalysisWarningsModal from './AnalysisWarningsModal'; | |||
export interface HeaderMetaProps { | |||
currentTask?: Task; | |||
currentTaskOnSameBranch?: boolean; | |||
component: Component; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onWarningDismiss: () => void; | |||
warnings: TaskWarning[]; | |||
} | |||
export function AnalysisStatus(props: HeaderMetaProps) { | |||
const { component, currentTask, currentTaskOnSameBranch, isInProgress, isPending, warnings } = | |||
props; | |||
const { component, currentTask, isInProgress, isPending } = props; | |||
const { data: warnings, isLoading } = useBranchWarrningQuery(component); | |||
const [modalIsVisible, setDisplayModal] = React.useState(false); | |||
const openModal = React.useCallback(() => { | |||
@@ -73,7 +71,6 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
<AnalysisErrorModal | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTaskOnSameBranch} | |||
onClose={closeModal} | |||
/> | |||
)} | |||
@@ -81,7 +78,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
); | |||
} | |||
if (warnings.length > 0) { | |||
if (!isLoading && warnings && warnings.length > 0) { | |||
return ( | |||
<> | |||
<FlagMessage variant="warning"> | |||
@@ -91,13 +88,7 @@ export function AnalysisStatus(props: HeaderMetaProps) { | |||
</Link> | |||
</FlagMessage> | |||
{modalIsVisible && ( | |||
<AnalysisWarningsModal | |||
componentKey={component.key} | |||
onClose={closeModal} | |||
taskId={currentTask?.id} | |||
onWarningDismiss={props.onWarningDismiss} | |||
warnings={warnings} | |||
/> | |||
<AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} /> | |||
)} | |||
</> | |||
); |
@@ -0,0 +1,103 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 { | |||
DangerButtonSecondary, | |||
DeferredSpinner, | |||
FlagMessage, | |||
HtmlFormatter, | |||
Modal, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { sanitizeStringRestricted } from '../../../../helpers/sanitize'; | |||
import { useDismissBranchWarningMutation } from '../../../../queries/branch'; | |||
import { TaskWarning } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { CurrentUser } from '../../../../types/users'; | |||
import withCurrentUserContext from '../../current-user/withCurrentUserContext'; | |||
interface Props { | |||
component: Component; | |||
currentUser: CurrentUser; | |||
onClose: () => void; | |||
warnings: TaskWarning[]; | |||
} | |||
export function AnalysisWarningsModal(props: Props) { | |||
const { component, currentUser, warnings } = props; | |||
const { mutate, isLoading, variables } = useDismissBranchWarningMutation(); | |||
const handleDismissMessage = (messageKey: string) => { | |||
mutate({ component, key: messageKey }); | |||
}; | |||
const body = ( | |||
<> | |||
{warnings.map(({ dismissable, key, message }) => ( | |||
<React.Fragment key={key}> | |||
<div className="sw-flex sw-items-center sw-mt-2"> | |||
<FlagMessage variant="warning"> | |||
<HtmlFormatter> | |||
<span | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')), | |||
}} | |||
/> | |||
</HtmlFormatter> | |||
</FlagMessage> | |||
</div> | |||
<div> | |||
{dismissable && currentUser.isLoggedIn && ( | |||
<div className="sw-mt-4"> | |||
<DangerButtonSecondary | |||
disabled={Boolean(isLoading)} | |||
onClick={() => { | |||
handleDismissMessage(key); | |||
}} | |||
> | |||
{translate('dismiss_permanently')} | |||
</DangerButtonSecondary> | |||
<DeferredSpinner | |||
className="sw-ml-2" | |||
loading={isLoading && variables?.key === key} | |||
/> | |||
</div> | |||
)} | |||
</div> | |||
</React.Fragment> | |||
))} | |||
</> | |||
); | |||
return ( | |||
<Modal | |||
headerTitle={translate('warnings')} | |||
onClose={props.onClose} | |||
body={body} | |||
primaryButton={null} | |||
secondaryButtonLabel={translate('close')} | |||
/> | |||
); | |||
} | |||
export default withCurrentUserContext(AnalysisWarningsModal); |
@@ -24,9 +24,8 @@ import { | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingResponse, | |||
} from '../../../../types/alm-settings'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Task, TaskWarning } from '../../../../types/tasks'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import RecentHistory from '../../RecentHistory'; | |||
import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif'; | |||
@@ -35,34 +34,17 @@ import HeaderMeta from './HeaderMeta'; | |||
import Menu from './Menu'; | |||
export interface ComponentNavProps { | |||
branchLikes: BranchLike[]; | |||
currentBranchLike: BranchLike | undefined; | |||
component: Component; | |||
currentTask?: Task; | |||
currentTaskOnSameBranch?: boolean; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onWarningDismiss: () => void; | |||
projectBinding?: ProjectAlmBindingResponse; | |||
projectBindingErrors?: ProjectAlmBindingConfigurationErrors; | |||
warnings: TaskWarning[]; | |||
} | |||
export default function ComponentNav(props: ComponentNavProps) { | |||
const { | |||
branchLikes, | |||
component, | |||
currentBranchLike, | |||
currentTask, | |||
currentTaskOnSameBranch, | |||
isInProgress, | |||
isPending, | |||
projectBinding, | |||
projectBindingErrors, | |||
warnings, | |||
} = props; | |||
const [displayProjectInfo, setDisplayProjectInfo] = React.useState(false); | |||
const { component, currentTask, isInProgress, isPending, projectBinding, projectBindingErrors } = | |||
props; | |||
React.useEffect(() => { | |||
const { breadcrumbs, key, name } = component; | |||
@@ -72,7 +54,6 @@ export default function ComponentNav(props: ComponentNavProps) { | |||
ComponentQualifier.Project, | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.Application, | |||
ComponentQualifier.Developper, | |||
].includes(qualifier as ComponentQualifier) | |||
) { | |||
RecentHistory.add(key, name, qualifier.toLowerCase()); | |||
@@ -88,34 +69,15 @@ export default function ComponentNav(props: ComponentNavProps) { | |||
<> | |||
<TopBar id="context-navigation" aria-label={translate('qualifier', component.qualifier)}> | |||
<div className="sw-min-h-10 sw-flex sw-justify-between"> | |||
<Header | |||
branchLikes={branchLikes} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
projectBinding={projectBinding} | |||
/> | |||
<Header component={component} projectBinding={projectBinding} /> | |||
<HeaderMeta | |||
branchLike={currentBranchLike} | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTaskOnSameBranch} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
onWarningDismiss={props.onWarningDismiss} | |||
warnings={warnings} | |||
/> | |||
</div> | |||
<Menu | |||
branchLike={currentBranchLike} | |||
branchLikes={branchLikes} | |||
component={component} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
onToggleProjectInfo={() => { | |||
setDisplayProjectInfo(!displayProjectInfo); | |||
}} | |||
projectInfoDisplayed={displayProjectInfo} | |||
/> | |||
<Menu component={component} isInProgress={isInProgress} isPending={isPending} /> | |||
</TopBar> | |||
{prDecoNotifComponent} | |||
</> |
@@ -19,38 +19,26 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { ProjectAlmBindingResponse } from '../../../../types/alm-settings'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { Component } from '../../../../types/types'; | |||
import { CurrentUser } from '../../../../types/users'; | |||
import withCurrentUserContext from '../../current-user/withCurrentUserContext'; | |||
import BranchLikeNavigation from './branch-like/BranchLikeNavigation'; | |||
import { Breadcrumb } from './Breadcrumb'; | |||
import BranchLikeNavigation from './branch-like/BranchLikeNavigation'; | |||
export interface HeaderProps { | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
currentBranchLike: BranchLike | undefined; | |||
currentUser: CurrentUser; | |||
projectBinding?: ProjectAlmBindingResponse; | |||
} | |||
export function Header(props: HeaderProps) { | |||
const { branchLikes, component, currentBranchLike, currentUser, projectBinding } = props; | |||
const { component, currentUser, projectBinding } = props; | |||
return ( | |||
<div className="sw-flex sw-flex-shrink sw-items-center"> | |||
<Breadcrumb component={component} currentUser={currentUser} /> | |||
{currentBranchLike && ( | |||
<> | |||
<span className="slash-separator sw-mx-2" /> | |||
<BranchLikeNavigation | |||
branchLikes={branchLikes} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
projectBinding={projectBinding} | |||
/> | |||
</> | |||
)} | |||
<BranchLikeNavigation component={component} projectBinding={projectBinding} /> | |||
</div> | |||
); | |||
} |
@@ -22,8 +22,8 @@ import * as React from 'react'; | |||
import HomePageSelect from '../../../../components/controls/HomePageSelect'; | |||
import { isBranch, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { translateWithParameters } from '../../../../helpers/l10n'; | |||
import { BranchLike } from '../../../../types/branch-like'; | |||
import { Task, TaskWarning } from '../../../../types/tasks'; | |||
import { useBranchesQuery } from '../../../../queries/branch'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { Component } from '../../../../types/types'; | |||
import { CurrentUser, isLoggedIn } from '../../../../types/users'; | |||
import withCurrentUserContext from '../../current-user/withCurrentUserContext'; | |||
@@ -32,28 +32,17 @@ import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMe | |||
import { getCurrentPage } from './utils'; | |||
export interface HeaderMetaProps { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
currentUser: CurrentUser; | |||
currentTask?: Task; | |||
currentTaskOnSameBranch?: boolean; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onWarningDismiss: () => void; | |||
warnings: TaskWarning[]; | |||
} | |||
export function HeaderMeta(props: HeaderMetaProps) { | |||
const { | |||
branchLike, | |||
component, | |||
currentUser, | |||
currentTask, | |||
currentTaskOnSameBranch, | |||
isInProgress, | |||
isPending, | |||
warnings, | |||
} = props; | |||
const { component, currentUser, currentTask, isInProgress, isPending } = props; | |||
const { data: { branchLike } = {} } = useBranchesQuery(component); | |||
const isABranch = isBranch(branchLike); | |||
@@ -64,11 +53,8 @@ export function HeaderMeta(props: HeaderMetaProps) { | |||
<AnalysisStatus | |||
component={component} | |||
currentTask={currentTask} | |||
currentTaskOnSameBranch={currentTaskOnSameBranch} | |||
isInProgress={isInProgress} | |||
isPending={isPending} | |||
onWarningDismiss={props.onWarningDismiss} | |||
warnings={warnings} | |||
/> | |||
{branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />} | |||
{component.version !== undefined && isABranch && ( |
@@ -30,8 +30,14 @@ import Tooltip from '../../../../components/controls/Tooltip'; | |||
import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; | |||
import { BranchLike, BranchParameters } from '../../../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; | |||
import { useBranchesQuery } from '../../../../queries/branch'; | |||
import { BranchParameters } from '../../../../types/branch-like'; | |||
import { | |||
ComponentQualifier, | |||
isApplication, | |||
isPortfolioLike, | |||
isProject, | |||
} from '../../../../types/component'; | |||
import { Feature } from '../../../../types/features'; | |||
import { Component, Dict, Extension } from '../../../../types/types'; | |||
import withAvailableFeatures, { | |||
@@ -55,84 +61,39 @@ const SETTINGS_URLS = [ | |||
]; | |||
interface Props extends WithAvailableFeaturesProps { | |||
branchLike: BranchLike | undefined; | |||
branchLikes: BranchLike[] | undefined; | |||
component: Component; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
onToggleProjectInfo: () => void; | |||
projectInfoDisplayed: boolean; | |||
} | |||
type Query = BranchParameters & { id: string }; | |||
export class Menu extends React.PureComponent<Props> { | |||
projectInfoLink: HTMLElement | null = null; | |||
componentDidUpdate(prevProps: Props) { | |||
if ( | |||
prevProps.projectInfoDisplayed && | |||
!this.props.projectInfoDisplayed && | |||
this.projectInfoLink | |||
) { | |||
this.projectInfoLink.focus(); | |||
} | |||
} | |||
export function Menu(props: Props) { | |||
const { component, isInProgress, isPending } = props; | |||
const { extensions = [], canBrowseAllChildProjects, qualifier, configuration = {} } = component; | |||
const { data: { branchLikes, branchLike } = { branchLikes: [] } } = useBranchesQuery(component); | |||
const isApplicationChildInaccessble = isApplication(qualifier) && !canBrowseAllChildProjects; | |||
hasAnalysis = () => { | |||
const { branchLikes = [], component, isInProgress, isPending } = this.props; | |||
const hasAnalysis = () => { | |||
const hasBranches = branchLikes.length > 1; | |||
return hasBranches || isInProgress || isPending || component.analysisDate !== undefined; | |||
}; | |||
isProject = () => { | |||
return this.props.component.qualifier === ComponentQualifier.Project; | |||
}; | |||
isDeveloper = () => { | |||
return this.props.component.qualifier === ComponentQualifier.Developper; | |||
}; | |||
isPortfolio = () => { | |||
const { qualifier } = this.props.component; | |||
return isPortfolioLike(qualifier); | |||
}; | |||
isApplication = () => { | |||
return this.props.component.qualifier === ComponentQualifier.Application; | |||
}; | |||
isAllChildProjectAccessible = () => { | |||
return Boolean(this.props.component.canBrowseAllChildProjects); | |||
}; | |||
const isGovernanceEnabled = extensions.some((extension) => | |||
extension.key.startsWith('governance/') | |||
); | |||
isApplicationChildInaccessble = () => { | |||
return this.isApplication() && !this.isAllChildProjectAccessible(); | |||
const getQuery = (): Query => { | |||
return { id: component.key, ...getBranchLikeQuery(branchLike) }; | |||
}; | |||
isGovernanceEnabled = () => { | |||
const { | |||
component: { extensions }, | |||
} = this.props; | |||
return extensions && extensions.some((extension) => extension.key.startsWith('governance/')); | |||
}; | |||
getConfiguration = () => { | |||
return this.props.component.configuration || {}; | |||
}; | |||
getQuery = (): Query => { | |||
return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) }; | |||
}; | |||
renderLinkWhenInaccessibleChild(label: React.ReactNode) { | |||
const renderLinkWhenInaccessibleChild = (label: React.ReactNode) => { | |||
return ( | |||
<li> | |||
<Tooltip | |||
overlay={translateWithParameters( | |||
'layout.all_project_must_be_accessible', | |||
translate('qualifier', this.props.component.qualifier) | |||
translate('qualifier', qualifier) | |||
)} | |||
> | |||
<a aria-disabled="true" className="disabled-link"> | |||
@@ -141,9 +102,9 @@ export class Menu extends React.PureComponent<Props> { | |||
</Tooltip> | |||
</li> | |||
); | |||
} | |||
}; | |||
renderMenuLink = ({ | |||
const renderMenuLink = ({ | |||
label, | |||
pathname, | |||
additionalQueryParams = {}, | |||
@@ -152,13 +113,11 @@ export class Menu extends React.PureComponent<Props> { | |||
pathname: string; | |||
additionalQueryParams?: Dict<string>; | |||
}) => { | |||
const hasAnalysis = this.hasAnalysis(); | |||
const isApplicationChildInaccessble = this.isApplicationChildInaccessble(); | |||
const query = this.getQuery(); | |||
const query = getQuery(); | |||
if (isApplicationChildInaccessble) { | |||
return this.renderLinkWhenInaccessibleChild(label); | |||
return renderLinkWhenInaccessibleChild(label); | |||
} | |||
return hasAnalysis ? ( | |||
return hasAnalysis() ? ( | |||
<NavBarTabLink | |||
to={{ | |||
pathname, | |||
@@ -171,86 +130,82 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderDashboardLink = () => { | |||
const { id, ...branchLike } = this.getQuery(); | |||
const renderDashboardLink = () => { | |||
const { id, ...branchLike } = getQuery(); | |||
if (this.isPortfolio()) { | |||
return this.isGovernanceEnabled() ? ( | |||
if (isPortfolioLike(qualifier)) { | |||
return isGovernanceEnabled ? ( | |||
<NavBarTabLink to={getPortfolioUrl(id)} text={translate('overview.page')} /> | |||
) : null; | |||
} | |||
const isApplicationChildInaccessble = this.isApplicationChildInaccessble(); | |||
if (isApplicationChildInaccessble) { | |||
return this.renderLinkWhenInaccessibleChild(translate('overview.page')); | |||
return renderLinkWhenInaccessibleChild(translate('overview.page')); | |||
} | |||
return ( | |||
<NavBarTabLink to={getProjectQueryUrl(id, branchLike)} text={translate('overview.page')} /> | |||
); | |||
}; | |||
renderBreakdownLink = () => { | |||
return this.isPortfolio() && this.isGovernanceEnabled() | |||
? this.renderMenuLink({ | |||
const renderBreakdownLink = () => { | |||
return isPortfolioLike(qualifier) && isGovernanceEnabled | |||
? renderMenuLink({ | |||
label: translate('portfolio_breakdown.page'), | |||
pathname: '/code', | |||
}) | |||
: null; | |||
}; | |||
renderCodeLink = () => { | |||
if (this.isPortfolio() || this.isDeveloper()) { | |||
const renderCodeLink = () => { | |||
if (isPortfolioLike(qualifier)) { | |||
return null; | |||
} | |||
const label = this.isApplication() ? translate('view_projects.page') : translate('code.page'); | |||
const label = isApplication(qualifier) | |||
? translate('view_projects.page') | |||
: translate('code.page'); | |||
return this.renderMenuLink({ label, pathname: '/code' }); | |||
return renderMenuLink({ label, pathname: '/code' }); | |||
}; | |||
renderActivityLink = () => { | |||
const { branchLike } = this.props; | |||
const renderActivityLink = () => { | |||
if (isPullRequest(branchLike)) { | |||
return null; | |||
} | |||
return this.renderMenuLink({ | |||
return renderMenuLink({ | |||
label: translate('project_activity.page'), | |||
pathname: '/project/activity', | |||
}); | |||
}; | |||
renderIssuesLink = () => { | |||
return this.renderMenuLink({ | |||
const renderIssuesLink = () => { | |||
return renderMenuLink({ | |||
label: translate('issues.page'), | |||
pathname: '/project/issues', | |||
additionalQueryParams: { resolved: 'false' }, | |||
}); | |||
}; | |||
renderComponentMeasuresLink = () => { | |||
return this.renderMenuLink({ | |||
const renderComponentMeasuresLink = () => { | |||
return renderMenuLink({ | |||
label: translate('layout.measures'), | |||
pathname: '/component_measures', | |||
}); | |||
}; | |||
renderSecurityHotspotsLink = () => { | |||
const isPortfolio = this.isPortfolio(); | |||
const renderSecurityHotspotsLink = () => { | |||
const isPortfolio = isPortfolioLike(qualifier); | |||
return ( | |||
!isPortfolio && | |||
this.renderMenuLink({ | |||
renderMenuLink({ | |||
label: translate('layout.security_hotspots'), | |||
pathname: '/security_hotspots', | |||
}) | |||
); | |||
}; | |||
renderSecurityReports = () => { | |||
const { branchLike, component } = this.props; | |||
const { extensions = [] } = component; | |||
const renderSecurityReports = () => { | |||
if (isPullRequest(branchLike)) { | |||
return null; | |||
} | |||
@@ -263,26 +218,27 @@ export class Menu extends React.PureComponent<Props> { | |||
return null; | |||
} | |||
return this.renderMenuLink({ | |||
return renderMenuLink({ | |||
label: translate('layout.security_reports'), | |||
pathname: '/project/extension/securityreport/securityreport', | |||
}); | |||
}; | |||
renderAdministration = () => { | |||
const { branchLike, component } = this.props; | |||
const isProject = this.isProject(); | |||
const isPortfolio = this.isPortfolio(); | |||
const isApplication = this.isApplication(); | |||
const query = this.getQuery(); | |||
const renderAdministration = () => { | |||
const query = getQuery(); | |||
if (!this.getConfiguration().showSettings || isPullRequest(branchLike)) { | |||
if (!configuration.showSettings || isPullRequest(branchLike)) { | |||
return null; | |||
} | |||
const isSettingsActive = SETTINGS_URLS.some((url) => window.location.href.includes(url)); | |||
const adminLinks = this.renderAdministrationLinks(query, isProject, isApplication, isPortfolio); | |||
const adminLinks = renderAdministrationLinks( | |||
query, | |||
isProject(qualifier), | |||
isApplication(qualifier), | |||
isPortfolioLike(qualifier) | |||
); | |||
if (!adminLinks.some((link) => link != null)) { | |||
return null; | |||
} | |||
@@ -313,46 +269,43 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderAdministrationLinks = ( | |||
const renderAdministrationLinks = ( | |||
query: Query, | |||
isProject: boolean, | |||
isApplication: boolean, | |||
isPortfolio: boolean | |||
) => { | |||
return [ | |||
this.renderSettingsLink(query, isApplication, isPortfolio), | |||
this.renderBranchesLink(query, isProject), | |||
this.renderBaselineLink(query, isApplication, isPortfolio), | |||
...this.renderAdminExtensions(query, isApplication), | |||
this.renderImportExportLink(query, isProject), | |||
this.renderProfilesLink(query), | |||
this.renderQualityGateLink(query), | |||
this.renderLinksLink(query), | |||
this.renderPermissionsLink(query), | |||
this.renderBackgroundTasksLink(query), | |||
this.renderUpdateKeyLink(query), | |||
this.renderWebhooksLink(query, isProject), | |||
this.renderDeletionLink(query), | |||
renderSettingsLink(query, isApplication, isPortfolio), | |||
renderBranchesLink(query, isProject), | |||
renderBaselineLink(query, isApplication, isPortfolio), | |||
...renderAdminExtensions(query, isApplication), | |||
renderImportExportLink(query, isProject), | |||
renderProfilesLink(query), | |||
renderQualityGateLink(query), | |||
renderLinksLink(query), | |||
renderPermissionsLink(query), | |||
renderBackgroundTasksLink(query), | |||
renderUpdateKeyLink(query), | |||
renderWebhooksLink(query, isProject), | |||
renderDeletionLink(query), | |||
]; | |||
}; | |||
renderProjectInformationButton = () => { | |||
const isProject = this.isProject(); | |||
const isApplication = this.isApplication(); | |||
const label = translate(isProject ? 'project' : 'application', 'info.title'); | |||
const isApplicationChildInaccessble = this.isApplicationChildInaccessble(); | |||
const query = this.getQuery(); | |||
const renderProjectInformationButton = () => { | |||
const label = translate(isProject(qualifier) ? 'project' : 'application', 'info.title'); | |||
const query = getQuery(); | |||
if (isPullRequest(this.props.branchLike)) { | |||
if (isPullRequest(branchLike)) { | |||
return null; | |||
} | |||
if (isApplicationChildInaccessble) { | |||
return this.renderLinkWhenInaccessibleChild(label); | |||
return renderLinkWhenInaccessibleChild(label); | |||
} | |||
return ( | |||
(isProject || isApplication) && ( | |||
(isProject(qualifier) || isApplication(qualifier)) && ( | |||
<NavBarTabLink | |||
to={{ pathname: '/project/information', search: new URLSearchParams(query).toString() }} | |||
text={label} | |||
@@ -361,8 +314,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderSettingsLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { | |||
if (!this.getConfiguration().showSettings || isApplication || isPortfolio) { | |||
const renderSettingsLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { | |||
if (!configuration.showSettings || isApplication || isPortfolio) { | |||
return null; | |||
} | |||
return ( | |||
@@ -375,12 +328,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderBranchesLink = (query: Query, isProject: boolean) => { | |||
if ( | |||
!this.props.hasFeature(Feature.BranchSupport) || | |||
!isProject || | |||
!this.getConfiguration().showSettings | |||
) { | |||
const renderBranchesLink = (query: Query, isProject: boolean) => { | |||
if (!props.hasFeature(Feature.BranchSupport) || !isProject || !configuration.showSettings) { | |||
return null; | |||
} | |||
@@ -394,8 +343,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderBaselineLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { | |||
if (!this.getConfiguration().showSettings || isApplication || isPortfolio) { | |||
const renderBaselineLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => { | |||
if (!configuration.showSettings || isApplication || isPortfolio) { | |||
return null; | |||
} | |||
return ( | |||
@@ -408,7 +357,7 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderImportExportLink = (query: Query, isProject: boolean) => { | |||
const renderImportExportLink = (query: Query, isProject: boolean) => { | |||
if (!isProject) { | |||
return null; | |||
} | |||
@@ -425,8 +374,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderProfilesLink = (query: Query) => { | |||
if (!this.getConfiguration().showQualityProfiles) { | |||
const renderProfilesLink = (query: Query) => { | |||
if (!configuration.showQualityProfiles) { | |||
return null; | |||
} | |||
return ( | |||
@@ -442,8 +391,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderQualityGateLink = (query: Query) => { | |||
if (!this.getConfiguration().showQualityGates) { | |||
const renderQualityGateLink = (query: Query) => { | |||
if (!configuration.showQualityGates) { | |||
return null; | |||
} | |||
return ( | |||
@@ -456,8 +405,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderLinksLink = (query: Query) => { | |||
if (!this.getConfiguration().showLinks) { | |||
const renderLinksLink = (query: Query) => { | |||
if (!configuration.showLinks) { | |||
return null; | |||
} | |||
return ( | |||
@@ -470,8 +419,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderPermissionsLink = (query: Query) => { | |||
if (!this.getConfiguration().showPermissions) { | |||
const renderPermissionsLink = (query: Query) => { | |||
if (!configuration.showPermissions) { | |||
return null; | |||
} | |||
return ( | |||
@@ -484,8 +433,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderBackgroundTasksLink = (query: Query) => { | |||
if (!this.getConfiguration().showBackgroundTasks) { | |||
const renderBackgroundTasksLink = (query: Query) => { | |||
if (!configuration.showBackgroundTasks) { | |||
return null; | |||
} | |||
return ( | |||
@@ -501,8 +450,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderUpdateKeyLink = (query: Query) => { | |||
if (!this.getConfiguration().showUpdateKey) { | |||
const renderUpdateKeyLink = (query: Query) => { | |||
if (!configuration.showUpdateKey) { | |||
return null; | |||
} | |||
return ( | |||
@@ -515,8 +464,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderWebhooksLink = (query: Query, isProject: boolean) => { | |||
if (!this.getConfiguration().showSettings || !isProject) { | |||
const renderWebhooksLink = (query: Query, isProject: boolean) => { | |||
if (!configuration.showSettings || !isProject) { | |||
return null; | |||
} | |||
return ( | |||
@@ -529,10 +478,8 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderDeletionLink = (query: Query) => { | |||
const { qualifier } = this.props.component; | |||
if (!this.getConfiguration().showSettings) { | |||
const renderDeletionLink = (query: Query) => { | |||
if (!configuration.showSettings) { | |||
return null; | |||
} | |||
@@ -556,9 +503,9 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderExtension = ({ key, name }: Extension, isAdmin: boolean, baseQuery: Query) => { | |||
const renderExtension = ({ key, name }: Extension, isAdmin: boolean, baseQuery: Query) => { | |||
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; | |||
const query = { ...baseQuery, qualifier: this.props.component.qualifier }; | |||
const query = { ...baseQuery, qualifier }; | |||
return ( | |||
<ItemNavLink key={key} to={{ pathname, search: new URLSearchParams(query).toString() }}> | |||
{name} | |||
@@ -566,16 +513,15 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderAdminExtensions = (query: Query, isApplication: boolean) => { | |||
const extensions = this.getConfiguration().extensions || []; | |||
const renderAdminExtensions = (query: Query, isApplication: boolean) => { | |||
const extensions = component.configuration?.extensions ?? []; | |||
return extensions | |||
.filter((e) => !isApplication || e.key !== 'governance/console') | |||
.map((e) => this.renderExtension(e, true, query)); | |||
.map((e) => renderExtension(e, true, query)); | |||
}; | |||
renderExtensions = () => { | |||
const query = this.getQuery(); | |||
const extensions = this.props.component.extensions ?? []; | |||
const renderExtensions = () => { | |||
const query = getQuery(); | |||
const withoutSecurityExtension = extensions.filter( | |||
(extension) => | |||
!extension.key.startsWith('securityreport/') && !extension.key.startsWith('governance/') | |||
@@ -591,7 +537,7 @@ export class Menu extends React.PureComponent<Props> { | |||
id="component-navigation-more" | |||
size="auto" | |||
zLevel={PopupZLevel.Global} | |||
overlay={withoutSecurityExtension.map((e) => this.renderExtension(e, false, query))} | |||
overlay={withoutSecurityExtension.map((e) => renderExtension(e, false, query))} | |||
> | |||
{({ onToggleClick, open, a11yAttrs }) => ( | |||
<NavBarTabLink | |||
@@ -608,27 +554,25 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
render() { | |||
return ( | |||
<div className="sw-flex sw-justify-between sw-pt-4 it__navbar-tabs"> | |||
<NavBarTabs> | |||
{this.renderDashboardLink()} | |||
{this.renderBreakdownLink()} | |||
{this.renderIssuesLink()} | |||
{this.renderSecurityHotspotsLink()} | |||
{this.renderSecurityReports()} | |||
{this.renderComponentMeasuresLink()} | |||
{this.renderCodeLink()} | |||
{this.renderActivityLink()} | |||
{this.renderExtensions()} | |||
</NavBarTabs> | |||
<NavBarTabs> | |||
{this.renderAdministration()} | |||
{this.renderProjectInformationButton()} | |||
</NavBarTabs> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="sw-flex sw-justify-between sw-pt-4 it__navbar-tabs"> | |||
<NavBarTabs> | |||
{renderDashboardLink()} | |||
{renderBreakdownLink()} | |||
{renderIssuesLink()} | |||
{renderSecurityHotspotsLink()} | |||
{renderSecurityReports()} | |||
{renderComponentMeasuresLink()} | |||
{renderCodeLink()} | |||
{renderActivityLink()} | |||
{renderExtensions()} | |||
</NavBarTabs> | |||
<NavBarTabs> | |||
{renderAdministration()} | |||
{renderProjectInformationButton()} | |||
</NavBarTabs> | |||
</div> | |||
); | |||
} | |||
export default withAvailableFeatures(Menu); |
@@ -19,29 +19,39 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { AnalysisErrorMessage } from '../AnalysisErrorMessage'; | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => { | |||
handler.reset(); | |||
}); | |||
it('should work when error is on a different branch', () => { | |||
renderAnalysisErrorMessage({ | |||
currentTask: mockTask({ branch: 'branch-1.2' }), | |||
currentTaskOnSameBranch: false, | |||
}); | |||
expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument(); | |||
expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument(); | |||
}); | |||
it('should work for errors on Pull Requests', () => { | |||
renderAnalysisErrorMessage({ | |||
currentTask: mockTask({ pullRequest: '2342', pullRequestTitle: 'Fix stuff' }), | |||
currentTaskOnSameBranch: true, | |||
}); | |||
it('should work for errors on Pull Requests', async () => { | |||
renderAnalysisErrorMessage( | |||
{ | |||
currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }), | |||
}, | |||
undefined, | |||
'pullRequest=01&id=my-project' | |||
); | |||
expect(screen.getByText(/component_navigation.status.failed_X/)).toBeInTheDocument(); | |||
expect(screen.getByText(/2342 - Fix stuff/)).toBeInTheDocument(); | |||
expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument(); | |||
expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument(); | |||
}); | |||
it('should provide a link to admins', () => { | |||
@@ -67,7 +77,8 @@ it('should explain to admins how to get the staktrace', () => { | |||
function renderAnalysisErrorMessage( | |||
overrides: Partial<Parameters<typeof AnalysisErrorMessage>[0]> = {}, | |||
location = '/' | |||
location = '/', | |||
params?: string | |||
) { | |||
return renderApp( | |||
location, | |||
@@ -75,8 +86,8 @@ function renderAnalysisErrorMessage( | |||
component={mockComponent()} | |||
currentTask={mockTask()} | |||
onLeave={jest.fn()} | |||
currentTaskOnSameBranch | |||
{...overrides} | |||
/> | |||
/>, | |||
{ navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] } | |||
); | |||
} |
@@ -21,19 +21,12 @@ import { screen } from '@testing-library/react'; | |||
import React from 'react'; | |||
import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { TaskStatuses } from '../../../../../types/tasks'; | |||
import ComponentNav, { ComponentNavProps } from '../ComponentNav'; | |||
it('renders correctly when there are warnings', () => { | |||
renderComponentNav({ warnings: [mockTaskWarning()] }); | |||
expect( | |||
screen.getByText('project_navigation.analysis_status.warnings', { exact: false }) | |||
).toBeInTheDocument(); | |||
}); | |||
it('renders correctly when there is a background task in progress', () => { | |||
renderComponentNav({ isInProgress: true }); | |||
expect( | |||
@@ -74,15 +67,11 @@ function renderComponentNav(props: Partial<ComponentNavProps> = {}) { | |||
return renderApp( | |||
'/', | |||
<ComponentNav | |||
branchLikes={[]} | |||
component={mockComponent({ | |||
breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }], | |||
})} | |||
currentBranchLike={undefined} | |||
isInProgress={false} | |||
isPending={false} | |||
onWarningDismiss={jest.fn()} | |||
warnings={[]} | |||
{...props} | |||
/> | |||
); |
@@ -20,19 +20,15 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockProjectAlmBindingResponse } from '../../../../../helpers/mocks/alm-settings'; | |||
import { | |||
mockMainBranch, | |||
mockPullRequest, | |||
mockSetOfBranchAndPullRequestForBranchSelector, | |||
} from '../../../../../helpers/mocks/branch-like'; | |||
import { mockMainBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { AlmKeys } from '../../../../../types/alm-settings'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { BranchStatusContext } from '../../../branch-status/BranchStatusContext'; | |||
import { Header, HeaderProps } from '../Header'; | |||
jest.mock('../../../../../api/favorites', () => ({ | |||
@@ -40,34 +36,51 @@ jest.mock('../../../../../api/favorites', () => ({ | |||
removeFavorite: jest.fn().mockResolvedValue({}), | |||
})); | |||
it('should render correctly when there is only 1 branch', () => { | |||
renderHeader({ branchLikes: [mockMainBranch()] }); | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => handler.reset()); | |||
it('should render correctly when there is only 1 branch', async () => { | |||
handler.emptyBranchesAndPullRequest(); | |||
handler.addBranch(mockMainBranch({ status: { qualityGateStatus: 'OK' } })); | |||
renderHeader(); | |||
expect(await screen.findByLabelText('help-tooltip')).toBeInTheDocument(); | |||
expect(screen.getByText('project')).toBeInTheDocument(); | |||
expect(screen.getByLabelText('help-tooltip')).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' }) | |||
await screen.findByRole('button', { name: 'master overview.quality_gate_x.OK' }) | |||
).toBeDisabled(); | |||
}); | |||
it('should render correctly when there are multiple branch', async () => { | |||
const user = userEvent.setup(); | |||
renderHeader(); | |||
expect(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })).toBeEnabled(); | |||
expect( | |||
await screen.findByRole('button', { name: 'main overview.quality_gate_x.OK' }) | |||
).toBeEnabled(); | |||
expect(screen.queryByLabelText('help-tooltip')).not.toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })); | |||
await user.click(screen.getByRole('button', { name: 'main overview.quality_gate_x.OK' })); | |||
expect(screen.getByText('branches.main_branch')).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' }) | |||
screen.getByRole('menuitem', { | |||
name: '03 – TEST-193 dumb commit overview.quality_gate_x.ERROR ERROR', | |||
}) | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('menuitem', { | |||
name: '01 – TEST-191 update master overview.quality_gate_x.OK OK', | |||
}) | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('menuitem', { name: 'normal-branch overview.quality_gate_x.ERROR ERROR' }) | |||
).toBeInTheDocument(); | |||
expect(screen.getByRole('menuitem', { name: 'branch-3' })).toBeInTheDocument(); | |||
expect(screen.getByRole('menuitem', { name: '1 – PR-1' })).toBeInTheDocument(); | |||
expect(screen.getByRole('menuitem', { name: '2 – PR-2' })).toBeInTheDocument(); | |||
await user.click( | |||
screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' }) | |||
screen.getByRole('menuitem', { name: 'normal-branch overview.quality_gate_x.ERROR ERROR' }) | |||
); | |||
expect(screen.getByText('/dashboard?branch=branch-2&id=my-project')).toBeInTheDocument(); | |||
expect(screen.getByText('/dashboard?branch=normal-branch&id=header-project')).toBeInTheDocument(); | |||
}); | |||
it('should show manage branch and pull request button for admin', async () => { | |||
@@ -75,16 +88,17 @@ it('should show manage branch and pull request button for admin', async () => { | |||
renderHeader({ | |||
currentUser: mockLoggedInUser(), | |||
component: mockComponent({ | |||
key: 'header-project', | |||
configuration: { showSettings: true }, | |||
breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }], | |||
}), | |||
}); | |||
await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })); | |||
await user.click(await screen.findByRole('button', { name: 'main overview.quality_gate_x.OK' })); | |||
expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toHaveAttribute( | |||
'href', | |||
'/project/branches?id=my-project' | |||
'/project/branches?id=header-project' | |||
); | |||
}); | |||
@@ -104,45 +118,41 @@ it('should render favorite button if the user is logged in', async () => { | |||
it.each([['github'], ['gitlab'], ['bitbucket'], ['azure']])( | |||
'should show correct %s links for a PR', | |||
(alm: string) => { | |||
renderHeader({ | |||
currentUser: mockLoggedInUser(), | |||
currentBranchLike: mockPullRequest({ | |||
key: '1', | |||
title: 'PR-1', | |||
status: { qualityGateStatus: 'OK' }, | |||
url: alm, | |||
}), | |||
branchLikes: [ | |||
mockPullRequest({ | |||
key: '1', | |||
title: 'PR-1', | |||
status: { qualityGateStatus: 'OK' }, | |||
url: alm, | |||
}), | |||
], | |||
}); | |||
const image = screen.getByAltText(alm); | |||
async (alm: string) => { | |||
handler.emptyBranchesAndPullRequest(); | |||
handler.addPullRequest(mockPullRequest({ url: alm })); | |||
renderHeader( | |||
{ | |||
currentUser: mockLoggedInUser(), | |||
}, | |||
undefined, | |||
'pullRequest=1001&id=compa' | |||
); | |||
const image = await screen.findByAltText(alm); | |||
expect(image).toBeInTheDocument(); | |||
expect(image).toHaveAttribute('src', `/images/alm/${alm}.svg`); | |||
} | |||
); | |||
it('should show the correct help tooltip for applications', () => { | |||
it('should show the correct help tooltip for applications', async () => { | |||
handler.emptyBranchesAndPullRequest(); | |||
handler.addBranch(mockMainBranch()); | |||
renderHeader({ | |||
currentUser: mockLoggedInUser(), | |||
component: mockComponent({ | |||
key: 'header-project', | |||
configuration: { showSettings: true }, | |||
breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Application }], | |||
qualifier: 'APP', | |||
}), | |||
branchLikes: [mockMainBranch()], | |||
}); | |||
expect(screen.getByText('application.branches.help')).toBeInTheDocument(); | |||
expect(await screen.findByText('application.branches.help')).toBeInTheDocument(); | |||
expect(screen.getByText('application.branches.link')).toBeInTheDocument(); | |||
}); | |||
it('should show the correct help tooltip when branch support is not enabled', () => { | |||
it('should show the correct help tooltip when branch support is not enabled', async () => { | |||
handler.emptyBranchesAndPullRequest(); | |||
handler.addBranch(mockMainBranch()); | |||
renderHeader( | |||
{ | |||
currentUser: mockLoggedInUser(), | |||
@@ -154,47 +164,29 @@ it('should show the correct help tooltip when branch support is not enabled', () | |||
}, | |||
[] | |||
); | |||
expect(screen.getByText('branch_like_navigation.no_branch_support.title.mr')).toBeInTheDocument(); | |||
expect( | |||
await screen.findByText('branch_like_navigation.no_branch_support.title.mr') | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByText('branch_like_navigation.no_branch_support.content_x.mr.alm.gitlab') | |||
).toBeInTheDocument(); | |||
}); | |||
function renderHeader(props?: Partial<HeaderProps>, featureList = [Feature.BranchSupport]) { | |||
const branchLikes = mockSetOfBranchAndPullRequestForBranchSelector(); | |||
function renderHeader( | |||
props?: Partial<HeaderProps>, | |||
featureList = [Feature.BranchSupport], | |||
params?: string | |||
) { | |||
return renderApp( | |||
'/', | |||
<BranchStatusContext.Provider | |||
value={{ | |||
branchStatusByComponent: { | |||
'my-project': { | |||
'branch-branch-1': { | |||
status: 'OK', | |||
}, | |||
'branch-branch-2': { | |||
status: 'ERROR', | |||
}, | |||
}, | |||
}, | |||
fetchBranchStatus: () => { | |||
/*noop*/ | |||
}, | |||
updateBranchStatus: () => { | |||
/*noop*/ | |||
}, | |||
}} | |||
> | |||
<Header | |||
branchLikes={branchLikes} | |||
component={mockComponent({ | |||
breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }], | |||
})} | |||
currentBranchLike={branchLikes[0]} | |||
currentUser={mockCurrentUser()} | |||
{...props} | |||
/> | |||
</BranchStatusContext.Provider>, | |||
{ featureList } | |||
<Header | |||
component={mockComponent({ | |||
key: 'header-project', | |||
breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }], | |||
})} | |||
currentUser={mockCurrentUser()} | |||
{...props} | |||
/>, | |||
{ featureList, navigateTo: params ? `/?id=header-project&${params}` : '/?id=header-project' } | |||
); | |||
} |
@@ -20,23 +20,39 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { mockBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like'; | |||
import { getAnalysisStatus } from '../../../../../api/ce'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks'; | |||
import { mockTask } from '../../../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; | |||
import { renderApp } from '../../../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { TaskStatuses } from '../../../../../types/tasks'; | |||
import { CurrentUser } from '../../../../../types/users'; | |||
import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta'; | |||
jest.mock('../../../../../api/ce'); | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => handler.reset()); | |||
it('should render correctly for a branch with warnings', async () => { | |||
const user = userEvent.setup(); | |||
jest.mocked(getAnalysisStatus).mockResolvedValue({ | |||
component: { | |||
warnings: [{ dismissable: false, key: 'key', message: 'bar' }], | |||
key: 'compkey', | |||
name: 'me', | |||
}, | |||
}); | |||
renderHeaderMeta({}, undefined, 'branch=normal-branch&id=my-project'); | |||
renderHeaderMeta(); | |||
expect(screen.getByText('version_x.0.0.1')).toBeInTheDocument(); | |||
expect(await screen.findByText('version_x.0.0.1')).toBeInTheDocument(); | |||
expect(screen.getByText('project_navigation.analysis_status.warnings')).toBeInTheDocument(); | |||
expect( | |||
await screen.findByText('project_navigation.analysis_status.warnings') | |||
).toBeInTheDocument(); | |||
await user.click(screen.getByText('project_navigation.analysis_status.details_link')); | |||
@@ -44,7 +60,14 @@ it('should render correctly for a branch with warnings', async () => { | |||
}); | |||
it('should handle a branch with missing version and no warnings', () => { | |||
renderHeaderMeta({ component: mockComponent({ version: undefined }), warnings: [] }); | |||
jest.mocked(getAnalysisStatus).mockResolvedValue({ | |||
component: { | |||
warnings: [], | |||
key: 'compkey', | |||
name: 'me', | |||
}, | |||
}); | |||
renderHeaderMeta({ component: mockComponent({ version: undefined }) }); | |||
expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument(); | |||
@@ -60,22 +83,20 @@ it('should render correctly with a failed analysis', async () => { | |||
}), | |||
}); | |||
expect(screen.getByText('project_navigation.analysis_status.failed')).toBeInTheDocument(); | |||
expect(await screen.findByText('project_navigation.analysis_status.failed')).toBeInTheDocument(); | |||
await user.click(screen.getByText('project_navigation.analysis_status.details_link')); | |||
expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument(); | |||
}); | |||
it('should render correctly for a pull request', () => { | |||
renderHeaderMeta({ | |||
branchLike: mockPullRequest({ | |||
url: 'https://example.com/pull/1234', | |||
}), | |||
}); | |||
it('should render correctly for a pull request', async () => { | |||
renderHeaderMeta({}, undefined, 'pullRequest=01&id=my-project'); | |||
expect( | |||
await screen.findByText('branch_like_navigation.for_merge_into_x_from_y') | |||
).toBeInTheDocument(); | |||
expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument(); | |||
expect(screen.getByText('branch_like_navigation.for_merge_into_x_from_y')).toBeInTheDocument(); | |||
}); | |||
it('should render correctly when the user is not logged in', () => { | |||
@@ -87,20 +108,12 @@ it('should render correctly when the user is not logged in', () => { | |||
function renderHeaderMeta( | |||
props: Partial<HeaderMetaProps> = {}, | |||
currentUser: CurrentUser = mockLoggedInUser() | |||
currentUser: CurrentUser = mockLoggedInUser(), | |||
params?: string | |||
) { | |||
return renderApp( | |||
'/', | |||
<HeaderMeta | |||
branchLike={mockBranch()} | |||
component={mockComponent({ version: '0.0.1' })} | |||
onWarningDismiss={jest.fn()} | |||
warnings={[ | |||
mockTaskWarning({ key: '1', message: 'ERROR_1' }), | |||
mockTaskWarning({ key: '2', message: 'ERROR_2' }), | |||
]} | |||
{...props} | |||
/>, | |||
{ currentUser } | |||
); | |||
return renderApp('/', <HeaderMeta component={mockComponent({ version: '0.0.1' })} {...props} />, { | |||
currentUser, | |||
navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project', | |||
featureList: [Feature.BranchSupport], | |||
}); | |||
} |
@@ -20,22 +20,24 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { | |||
mockBranch, | |||
mockMainBranch, | |||
mockPullRequest, | |||
} from '../../../../../helpers/mocks/branch-like'; | |||
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | |||
import { ComponentPropsType } from '../../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { Menu } from '../Menu'; | |||
const handler = new BranchesServiceMock(); | |||
const BASE_COMPONENT = mockComponent({ | |||
analysisDate: '2019-12-01', | |||
key: 'foo', | |||
name: 'foo', | |||
}); | |||
beforeEach(() => handler.reset()); | |||
it('should render correctly', async () => { | |||
const user = userEvent.setup(); | |||
const component = { | |||
@@ -90,33 +92,37 @@ it('should render correctly when on a Portofolio', () => { | |||
expect(screen.getByRole('link', { name: 'portfolio_breakdown.page' })).toBeInTheDocument(); | |||
}); | |||
it('should render correctly when on a branch', () => { | |||
renderMenu({ | |||
branchLike: mockBranch(), | |||
component: { | |||
...BASE_COMPONENT, | |||
configuration: { showSettings: true }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }], | |||
it('should render correctly when on a branch', async () => { | |||
renderMenu( | |||
{ | |||
component: { | |||
...BASE_COMPONENT, | |||
configuration: { showSettings: true }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }], | |||
}, | |||
}, | |||
}); | |||
'branch=normal-branch' | |||
); | |||
expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument(); | |||
expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'project.info.title' })).toBeInTheDocument(); | |||
}); | |||
it('should render correctly when on a pull request', () => { | |||
renderMenu({ | |||
branchLike: mockPullRequest(), | |||
component: { | |||
...BASE_COMPONENT, | |||
configuration: { showSettings: true }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }], | |||
it('should render correctly when on a pull request', async () => { | |||
renderMenu( | |||
{ | |||
component: { | |||
...BASE_COMPONENT, | |||
configuration: { showSettings: true }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }], | |||
}, | |||
}, | |||
}); | |||
'pullRequest=01' | |||
); | |||
expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument(); | |||
expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument(); | |||
@@ -153,19 +159,16 @@ it('should disable links if application has inaccessible projects', () => { | |||
expect(screen.queryByRole('button', { name: 'application.info.title' })).not.toBeInTheDocument(); | |||
}); | |||
function renderMenu(props: Partial<Menu['props']> = {}) { | |||
const mainBranch = mockMainBranch(); | |||
function renderMenu(props: Partial<ComponentPropsType<typeof Menu>> = {}, params?: string) { | |||
return renderComponent( | |||
<Menu | |||
hasFeature={jest.fn().mockReturnValue(false)} | |||
branchLike={mainBranch} | |||
branchLikes={[mainBranch]} | |||
component={BASE_COMPONENT} | |||
isInProgress={false} | |||
isPending={false} | |||
onToggleProjectInfo={jest.fn()} | |||
projectInfoDisplayed={false} | |||
{...props} | |||
/> | |||
/>, | |||
params ? `/?${params}` : '/', | |||
{ featureList: [Feature.BranchSupport] } | |||
); | |||
} |
@@ -22,8 +22,8 @@ import * as React from 'react'; | |||
import EscKeydownHandler from '../../../../../components/controls/EscKeydownHandler'; | |||
import FocusOutHandler from '../../../../../components/controls/FocusOutHandler'; | |||
import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler'; | |||
import { useBranchesQuery } from '../../../../../queries/branch'; | |||
import { AlmKeys, ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../../../types/component'; | |||
import { Feature } from '../../../../../types/features'; | |||
import { Component } from '../../../../../types/types'; | |||
@@ -36,91 +36,95 @@ import Menu from './Menu'; | |||
import PRLink from './PRLink'; | |||
export interface BranchLikeNavigationProps extends WithAvailableFeaturesProps { | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
currentBranchLike: BranchLike; | |||
projectBinding?: ProjectAlmBindingResponse; | |||
} | |||
export function BranchLikeNavigation(props: BranchLikeNavigationProps) { | |||
const { | |||
branchLikes, | |||
component, | |||
component: { configuration }, | |||
currentBranchLike, | |||
projectBinding, | |||
} = props; | |||
const { data: { branchLikes, branchLike: currentBranchLike } = { branchLikes: [] } } = | |||
useBranchesQuery(component); | |||
const [isMenuOpen, setIsMenuOpen] = React.useState(false); | |||
if (currentBranchLike === undefined) { | |||
return null; | |||
} | |||
const isApplication = component.qualifier === ComponentQualifier.Application; | |||
const isGitLab = projectBinding !== undefined && projectBinding.alm === AlmKeys.GitLab; | |||
const [isMenuOpen, setIsMenuOpen] = React.useState(false); | |||
const branchSupportEnabled = props.hasFeature(Feature.BranchSupport); | |||
const canAdminComponent = configuration?.showSettings; | |||
const hasManyBranches = branchLikes.length >= 2; | |||
const isMenuEnabled = branchSupportEnabled && hasManyBranches; | |||
const currentBranchLikeElement = ( | |||
<CurrentBranchLike component={component} currentBranchLike={currentBranchLike} /> | |||
); | |||
const currentBranchLikeElement = <CurrentBranchLike currentBranchLike={currentBranchLike} />; | |||
const handleOutsideClick = () => { | |||
setIsMenuOpen(false); | |||
}; | |||
return ( | |||
<div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container"> | |||
<Popup | |||
allowResizing | |||
overlay={ | |||
isMenuOpen && ( | |||
<FocusOutHandler onFocusOut={handleOutsideClick}> | |||
<EscKeydownHandler onKeydown={handleOutsideClick}> | |||
<OutsideClickHandler onClickOutside={handleOutsideClick}> | |||
<Menu | |||
branchLikes={branchLikes} | |||
canAdminComponent={canAdminComponent} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
onClose={() => { | |||
setIsMenuOpen(false); | |||
}} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
</FocusOutHandler> | |||
) | |||
} | |||
placement={PopupPlacement.BottomLeft} | |||
zLevel={PopupZLevel.Global} | |||
> | |||
<ButtonSecondary | |||
className="sw-max-w-abs-350 sw-px-3" | |||
onClick={() => { | |||
setIsMenuOpen(!isMenuOpen); | |||
}} | |||
disabled={!isMenuEnabled} | |||
aria-expanded={isMenuOpen} | |||
aria-haspopup="menu" | |||
<> | |||
<span className="slash-separator sw-mx-2" /> | |||
<div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container"> | |||
<Popup | |||
allowResizing | |||
overlay={ | |||
isMenuOpen && ( | |||
<FocusOutHandler onFocusOut={handleOutsideClick}> | |||
<EscKeydownHandler onKeydown={handleOutsideClick}> | |||
<OutsideClickHandler onClickOutside={handleOutsideClick}> | |||
<Menu | |||
branchLikes={branchLikes} | |||
canAdminComponent={canAdminComponent} | |||
component={component} | |||
currentBranchLike={currentBranchLike} | |||
onClose={() => { | |||
setIsMenuOpen(false); | |||
}} | |||
/> | |||
</OutsideClickHandler> | |||
</EscKeydownHandler> | |||
</FocusOutHandler> | |||
) | |||
} | |||
placement={PopupPlacement.BottomLeft} | |||
zLevel={PopupZLevel.Global} | |||
> | |||
{currentBranchLikeElement} | |||
</ButtonSecondary> | |||
</Popup> | |||
<ButtonSecondary | |||
className="sw-max-w-abs-350 sw-px-3" | |||
onClick={() => { | |||
setIsMenuOpen(!isMenuOpen); | |||
}} | |||
disabled={!isMenuEnabled} | |||
aria-expanded={isMenuOpen} | |||
aria-haspopup="menu" | |||
> | |||
{currentBranchLikeElement} | |||
</ButtonSecondary> | |||
</Popup> | |||
<div className="sw-ml-2"> | |||
<BranchHelpTooltip | |||
component={component} | |||
isApplication={isApplication} | |||
projectBinding={projectBinding} | |||
hasManyBranches={hasManyBranches} | |||
canAdminComponent={canAdminComponent} | |||
branchSupportEnabled={branchSupportEnabled} | |||
isGitLab={isGitLab} | |||
/> | |||
</div> | |||
<div className="sw-ml-2"> | |||
<BranchHelpTooltip | |||
component={component} | |||
isApplication={isApplication} | |||
projectBinding={projectBinding} | |||
hasManyBranches={hasManyBranches} | |||
canAdminComponent={canAdminComponent} | |||
branchSupportEnabled={branchSupportEnabled} | |||
isGitLab={isGitLab} | |||
/> | |||
</div> | |||
<PRLink currentBranchLike={currentBranchLike} component={component} /> | |||
</div> | |||
<PRLink currentBranchLike={currentBranchLike} component={component} /> | |||
</div> | |||
</> | |||
); | |||
} | |||
@@ -22,16 +22,14 @@ import * as React from 'react'; | |||
import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon'; | |||
import { getBranchLikeDisplayName } from '../../../../../helpers/branch-like'; | |||
import { BranchLike, BranchStatusData } from '../../../../../types/branch-like'; | |||
import { Component } from '../../../../../types/types'; | |||
import QualityGateStatus from './QualityGateStatus'; | |||
export interface CurrentBranchLikeProps extends Pick<BranchStatusData, 'status'> { | |||
component: Component; | |||
currentBranchLike: BranchLike; | |||
} | |||
export function CurrentBranchLike(props: CurrentBranchLikeProps) { | |||
const { component, currentBranchLike } = props; | |||
const { currentBranchLike } = props; | |||
const displayName = getBranchLikeDisplayName(currentBranchLike); | |||
@@ -39,7 +37,7 @@ export function CurrentBranchLike(props: CurrentBranchLikeProps) { | |||
<div className="sw-flex sw-items-center text-ellipsis"> | |||
<BranchLikeIcon branchLike={currentBranchLike} /> | |||
<TextMuted text={displayName} className="sw-ml-3" /> | |||
<QualityGateStatus branchLike={currentBranchLike} component={component} className="sw-ml-4" /> | |||
<QualityGateStatus branchLike={currentBranchLike} className="sw-ml-4" /> | |||
<ChevronDownIcon className="sw-ml-1" /> | |||
</div> | |||
); |
@@ -176,7 +176,6 @@ export class Menu extends React.PureComponent<Props, State> { | |||
/> | |||
<MenuItemList | |||
branchLikeTree={branchLikesToDisplayTree} | |||
component={component} | |||
hasResults={hasResults} | |||
onSelect={this.handleOnSelect} | |||
selectedBranchLike={selectedBranchLike} |
@@ -24,12 +24,10 @@ import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon'; | |||
import { getBranchLikeDisplayName, isMainBranch } from '../../../../../helpers/branch-like'; | |||
import { translate } from '../../../../../helpers/l10n'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { Component } from '../../../../../types/types'; | |||
import QualityGateStatus from './QualityGateStatus'; | |||
export interface MenuItemProps { | |||
branchLike: BranchLike; | |||
component: Component; | |||
onSelect: (branchLike: BranchLike) => void; | |||
selected: boolean; | |||
indent: boolean; | |||
@@ -37,7 +35,7 @@ export interface MenuItemProps { | |||
} | |||
export function MenuItem(props: MenuItemProps) { | |||
const { branchLike, component, setSelectedNode, onSelect, selected, indent } = props; | |||
const { branchLike, setSelectedNode, onSelect, selected, indent } = props; | |||
const displayName = getBranchLikeDisplayName(branchLike); | |||
return ( | |||
@@ -64,7 +62,6 @@ export function MenuItem(props: MenuItemProps) { | |||
</div> | |||
<QualityGateStatus | |||
branchLike={branchLike} | |||
component={component} | |||
className="sw-flex sw-items-center sw-w-24" | |||
showStatusText | |||
/> |
@@ -24,12 +24,10 @@ import { getBranchLikeKey, isSameBranchLike } from '../../../../../helpers/branc | |||
import { translate } from '../../../../../helpers/l10n'; | |||
import { isDefined } from '../../../../../helpers/types'; | |||
import { BranchLike, BranchLikeTree } from '../../../../../types/branch-like'; | |||
import { Component } from '../../../../../types/types'; | |||
import MenuItem from './MenuItem'; | |||
export interface MenuItemListProps { | |||
branchLikeTree: BranchLikeTree; | |||
component: Component; | |||
hasResults: boolean; | |||
onSelect: (branchLike: BranchLike) => void; | |||
selectedBranchLike: BranchLike | undefined; | |||
@@ -45,12 +43,11 @@ export function MenuItemList(props: MenuItemListProps) { | |||
} | |||
}); | |||
const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props; | |||
const { branchLikeTree, hasResults, onSelect, selectedBranchLike } = props; | |||
const renderItem = (branchLike: BranchLike, indent = false) => ( | |||
<MenuItem | |||
branchLike={branchLike} | |||
component={component} | |||
key={getBranchLikeKey(branchLike)} | |||
onSelect={onSelect} | |||
selected={isSameBranchLike(branchLike, selectedBranchLike)} |
@@ -19,45 +19,34 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import { QualityGateIndicator } from 'design-system'; | |||
import React, { useContext } from 'react'; | |||
import { getBranchStatusByBranchLike } from '../../../../../helpers/branch-like'; | |||
import React from 'react'; | |||
import { translateWithParameters } from '../../../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../../../helpers/measures'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { MetricType } from '../../../../../types/metrics'; | |||
import { Component } from '../../../../../types/types'; | |||
import { BranchStatusContext } from '../../../branch-status/BranchStatusContext'; | |||
interface Props { | |||
component: Component; | |||
branchLike: BranchLike; | |||
className: string; | |||
showStatusText?: boolean; | |||
} | |||
export default function QualityGateStatus({ | |||
component, | |||
branchLike, | |||
className, | |||
showStatusText, | |||
}: Props) { | |||
const { branchStatusByComponent } = useContext(BranchStatusContext); | |||
const branchStatus = getBranchStatusByBranchLike( | |||
branchStatusByComponent, | |||
component.key, | |||
branchLike | |||
); | |||
export default function QualityGateStatus({ className, showStatusText, branchLike }: Props) { | |||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition | |||
if (!branchStatus || !branchStatus.status) { | |||
if (!branchLike.status?.qualityGateStatus) { | |||
return null; | |||
} | |||
const { status } = branchStatus; | |||
const formatted = formatMeasure(status, MetricType.Level); | |||
const formatted = formatMeasure(branchLike.status?.qualityGateStatus, MetricType.Level); | |||
const ariaLabel = translateWithParameters('overview.quality_gate_x', formatted); | |||
return ( | |||
<div className={classNames(`it__level-${status}`, className)}> | |||
<QualityGateIndicator status={status} className="sw-mr-2" ariaLabel={ariaLabel} size="sm" /> | |||
<div className={classNames(`it__level-${branchLike.status.qualityGateStatus}`, className)}> | |||
<QualityGateIndicator | |||
status={branchLike.status?.qualityGateStatus} | |||
className="sw-mr-2" | |||
ariaLabel={ariaLabel} | |||
size="sm" | |||
/> | |||
{showStatusText && <span>{formatted}</span>} | |||
</div> | |||
); |
@@ -27,9 +27,7 @@ import { CurrentUserContextInterface } from '../current-user/CurrentUserContext' | |||
import withCurrentUserContext from '../current-user/withCurrentUserContext'; | |||
import './PromotionNotification.css'; | |||
export interface PromotionNotificationProps extends CurrentUserContextInterface {} | |||
export function PromotionNotification(props: PromotionNotificationProps) { | |||
export function PromotionNotification(props: CurrentUserContextInterface) { | |||
const { currentUser } = props; | |||
if (!isLoggedIn(currentUser) || currentUser.dismissedNotices[NoticeType.SONARLINT_AD]) { |
@@ -23,7 +23,8 @@ import { dismissNotice } from '../../../../api/users'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { NoticeType } from '../../../../types/users'; | |||
import { PromotionNotification, PromotionNotificationProps } from '../PromotionNotification'; | |||
import { CurrentUserContextInterface } from '../../current-user/CurrentUserContext'; | |||
import { PromotionNotification } from '../PromotionNotification'; | |||
jest.mock('../../../../api/users', () => ({ | |||
dismissNotice: jest.fn().mockResolvedValue({}), | |||
@@ -67,7 +68,7 @@ it('should remove the toaster and navigate to sonarlint when click on learn more | |||
expect(updateDismissedNotices).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<PromotionNotificationProps> = {}) { | |||
function shallowRender(props: Partial<CurrentUserContextInterface> = {}) { | |||
return shallow( | |||
<PromotionNotification | |||
currentUser={mockCurrentUser()} |
@@ -25,20 +25,18 @@ import { | |||
Modal, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { dismissAnalysisWarning, getTask } from '../../api/ce'; | |||
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { sanitizeStringRestricted } from '../../helpers/sanitize'; | |||
import { TaskWarning } from '../../types/tasks'; | |||
import { CurrentUser } from '../../types/users'; | |||
import { dismissAnalysisWarning, getTask } from '../../../api/ce'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeStringRestricted } from '../../../helpers/sanitize'; | |||
import { TaskWarning } from '../../../types/tasks'; | |||
import { CurrentUser } from '../../../types/users'; | |||
interface Props { | |||
componentKey?: string; | |||
currentUser: CurrentUser; | |||
onClose: () => void; | |||
onWarningDismiss?: () => void; | |||
taskId?: string; | |||
warnings?: TaskWarning[]; | |||
taskId: string; | |||
} | |||
interface State { | |||
@@ -53,24 +51,20 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: !props.warnings, | |||
warnings: props.warnings || [], | |||
loading: false, | |||
warnings: [], | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (!this.props.warnings && this.props.taskId) { | |||
this.loadWarnings(this.props.taskId); | |||
} | |||
this.loadWarnings(this.props.taskId); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
const { taskId, warnings } = this.props; | |||
if (!warnings && taskId && prevProps.taskId !== taskId) { | |||
const { taskId } = this.props; | |||
if (prevProps.taskId !== taskId) { | |||
this.loadWarnings(taskId); | |||
} else if (warnings && prevProps.warnings !== warnings) { | |||
this.setState({ warnings }); | |||
} | |||
} | |||
@@ -86,13 +80,8 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> { | |||
} | |||
this.setState({ dismissedWarning: messageKey }); | |||
try { | |||
await dismissAnalysisWarning(componentKey, messageKey); | |||
if (this.props.onWarningDismiss) { | |||
this.props.onWarningDismiss(); | |||
} | |||
} catch (e) { | |||
// Noop | |||
} |
@@ -18,11 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import AnalysisWarningsModal from '../../../components/common/AnalysisWarningsModal'; | |||
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; | |||
import ConfirmModal from '../../../components/controls/ConfirmModal'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { Task, TaskStatuses } from '../../../types/tasks'; | |||
import AnalysisWarningsModal from './AnalysisWarningsModal'; | |||
import ScannerContext from './ScannerContext'; | |||
import Stacktrace from './Stacktrace'; | |||
@@ -17,17 +17,15 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { debounce, noop } from 'lodash'; | |||
import * as React from 'react'; | |||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | |||
import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
import { CodeScope, getCodeUrl, getProjectUrl } from '../../../helpers/urls'; | |||
import { withBranchLikes } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types'; | |||
import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types'; | |||
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; | |||
import '../code.css'; | |||
import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils'; | |||
@@ -35,8 +33,8 @@ import CodeAppRenderer from './CodeAppRenderer'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; | |||
location: Location; | |||
router: Router; | |||
metrics: Dict<Metric>; | |||
@@ -68,7 +66,6 @@ class CodeApp extends React.Component<Props, State> { | |||
total: 0, | |||
newCodeSelected: true, | |||
}; | |||
this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000); | |||
} | |||
componentDidMount() { | |||
@@ -184,10 +181,6 @@ class CodeApp extends React.Component<Props, State> { | |||
this.setState({ highlighted }); | |||
}; | |||
handleIssueChange = (_: Issue) => { | |||
this.refreshBranchStatus(); | |||
}; | |||
handleSearchClear = () => { | |||
this.setState({ searchResults: undefined }); | |||
}; | |||
@@ -223,13 +216,6 @@ class CodeApp extends React.Component<Props, State> { | |||
this.loadComponent(finalKey); | |||
}; | |||
refreshBranchStatus = () => { | |||
const { branchLike, component } = this.props; | |||
if (branchLike && component && isPullRequest(branchLike)) { | |||
this.props.fetchBranchStatus(branchLike, component.key).catch(noop); | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<CodeAppRenderer | |||
@@ -237,7 +223,6 @@ class CodeApp extends React.Component<Props, State> { | |||
{...this.state} | |||
handleGoToParent={this.handleGoToParent} | |||
handleHighlight={this.handleHighlight} | |||
handleIssueChange={this.handleIssueChange} | |||
handleLoadMore={this.handleLoadMore} | |||
handleSearchClear={this.handleSearchClear} | |||
handleSearchResults={this.handleSearchResults} | |||
@@ -248,6 +233,4 @@ class CodeApp extends React.Component<Props, State> { | |||
} | |||
} | |||
export default withRouter( | |||
withComponentContext(withBranchStatusActions(withMetricsContext(CodeApp))) | |||
); | |||
export default withRouter(withComponentContext(withMetricsContext(withBranchLikes(CodeApp)))); |
@@ -38,7 +38,7 @@ import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isApplication, isPortfolioLike } from '../../../types/component'; | |||
import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types'; | |||
import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types'; | |||
import '../code.css'; | |||
import { getCodeMetrics } from '../utils'; | |||
import CodeBreadcrumbs from './CodeBreadcrumbs'; | |||
@@ -63,7 +63,6 @@ interface Props { | |||
handleGoToParent: () => void; | |||
handleHighlight: (highlighted: ComponentMeasure) => void; | |||
handleIssueChange: (issue: Issue) => void; | |||
handleLoadMore: () => void; | |||
handleSearchClear: () => void; | |||
handleSearchResults: (searchResults: ComponentMeasure[]) => void; | |||
@@ -230,7 +229,6 @@ export default function CodeAppRenderer(props: Props) { | |||
isFile | |||
location={location} | |||
onGoToParent={props.handleGoToParent} | |||
onIssueChange={props.handleIssueChange} | |||
/> | |||
</div> | |||
)} |
@@ -18,18 +18,17 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; | |||
import { Location } from '../../../components/hoc/withRouter'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Issue, Measure } from '../../../types/types'; | |||
import { Measure } from '../../../types/types'; | |||
export interface SourceViewerWrapperProps { | |||
branchLike?: BranchLike; | |||
component: string; | |||
componentMeasures: Measure[] | undefined; | |||
location: Location; | |||
onIssueChange?: (issue: Issue) => void; | |||
} | |||
function SourceViewerWrapper(props: SourceViewerWrapperProps) { | |||
@@ -53,7 +52,6 @@ function SourceViewerWrapper(props: SourceViewerWrapperProps) { | |||
component={component} | |||
componentMeasures={componentMeasures} | |||
highlightedLine={finalLine} | |||
onIssueChange={props.onIssueChange} | |||
onLoaded={handleLoaded} | |||
showMeasures | |||
/> |
@@ -21,15 +21,16 @@ import { act, screen, waitFor, within } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { times } from 'lodash'; | |||
import selectEvent from 'react-select-event'; | |||
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; | |||
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; | |||
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | |||
import { MeasuresServiceMock } from '../../../api/mocks/MeasuresServiceMock'; | |||
import { mockPullRequest } from '../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockMeasure, mockMetric } from '../../../helpers/testMocks'; | |||
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole, byTestId, byText } from '../../../helpers/testSelector'; | |||
import { ComponentContextShape, ComponentQualifier } from '../../../types/component'; | |||
import { Feature } from '../../../types/features'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import routes from '../routes'; | |||
@@ -46,11 +47,13 @@ jest.mock('../../../api/metrics', () => { | |||
const componentsHandler = new ComponentsServiceMock(); | |||
const measuresHandler = new MeasuresServiceMock(); | |||
const issuesHandler = new IssuesServiceMock(); | |||
const branchHandler = new BranchesServiceMock(); | |||
afterEach(() => { | |||
componentsHandler.reset(); | |||
measuresHandler.reset(); | |||
issuesHandler.reset(); | |||
branchHandler.reset(); | |||
}); | |||
describe('rendering', () => { | |||
@@ -144,12 +147,10 @@ describe('rendering', () => { | |||
it('should render correctly if on a pull request and viewing coverage', async () => { | |||
const { ui } = getPageObject(); | |||
renderMeasuresApp('component_measures?id=foo&metric=coverage&pullRequest=1', { | |||
branchLike: mockPullRequest({ key: '1' }), | |||
}); | |||
renderMeasuresApp('component_measures?id=foo&metric=coverage&pullRequest=01'); | |||
await ui.appLoaded(); | |||
expect(ui.detailsUnavailableText.get()).toBeInTheDocument(); | |||
expect(await ui.detailsUnavailableText.find()).toBeInTheDocument(); | |||
}); | |||
it('should render a warning message if the user does not have access to all components', async () => { | |||
@@ -538,7 +539,7 @@ function renderMeasuresApp(navigateTo?: string, componentContext?: Partial<Compo | |||
return renderAppWithComponentContext( | |||
'component_measures', | |||
routes, | |||
{ navigateTo }, | |||
{ navigateTo, featureList: [Feature.BranchSupport] }, | |||
{ component: mockComponent({ key: 'foo' }), ...componentContext } | |||
); | |||
} |
@@ -27,12 +27,11 @@ import { | |||
themeBorder, | |||
themeColor, | |||
} from 'design-system'; | |||
import { debounce, keyBy } from 'lodash'; | |||
import { keyBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { getMeasuresWithPeriod } from '../../../api/measures'; | |||
import { getAllMetrics } from '../../../api/metrics'; | |||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; | |||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
@@ -40,18 +39,12 @@ import { enhanceMeasure } from '../../../components/measure/utils'; | |||
import '../../../components/search-navigator.css'; | |||
import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { MeasurePageView } from '../../../types/measures'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { | |||
ComponentMeasure, | |||
Dict, | |||
Issue, | |||
MeasureEnhanced, | |||
Metric, | |||
Period, | |||
} from '../../../types/types'; | |||
import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import '../style.css'; | |||
import { | |||
@@ -74,7 +67,6 @@ import MeasuresEmpty from './MeasuresEmpty'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: ComponentMeasure; | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; | |||
location: Location; | |||
router: Router; | |||
} | |||
@@ -97,7 +89,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
measures: [], | |||
metrics: {}, | |||
}; | |||
this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000); | |||
} | |||
componentDidMount() { | |||
@@ -180,10 +171,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
return metric; | |||
}; | |||
handleIssueChange = (_: Issue) => { | |||
this.refreshBranchStatus(); | |||
}; | |||
updateQuery = (newQuery: Partial<Query>) => { | |||
const query: Query = { ...parseQuery(this.props.location.query), ...newQuery }; | |||
@@ -206,13 +193,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
}); | |||
}; | |||
refreshBranchStatus = () => { | |||
const { branchLike, component } = this.props; | |||
if (branchLike && component && isPullRequest(branchLike)) { | |||
this.props.fetchBranchStatus(branchLike, component.key); | |||
} | |||
}; | |||
renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => { | |||
const { branchLike, component } = this.props; | |||
const { leakPeriod } = this.state; | |||
@@ -225,7 +205,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
domain={query.metric} | |||
leakPeriod={leakPeriod} | |||
metrics={this.state.metrics} | |||
onIssueChange={this.handleIssueChange} | |||
rootComponent={component} | |||
router={this.props.router} | |||
selected={query.selected} | |||
@@ -261,7 +240,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
branchLike={branchLike} | |||
leakPeriod={leakPeriod} | |||
metrics={this.state.metrics} | |||
onIssueChange={this.handleIssueChange} | |||
requestedMetric={metric} | |||
rootComponent={component} | |||
router={this.props.router} | |||
@@ -323,10 +301,11 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
* is that we can't use the usual withComponentContext HOC, because the type | |||
* of `component` isn't the same. It probably used to work because of the lazy loading | |||
*/ | |||
const WrappedApp = withRouter(withBranchStatusActions(ComponentMeasuresApp)); | |||
const WrappedApp = withRouter(ComponentMeasuresApp); | |||
function AppWithComponentContext() { | |||
const { branchLike, component } = React.useContext(ComponentContext); | |||
const { component } = React.useContext(ComponentContext); | |||
const { data: { branchLike } = {} } = useBranchesQuery(component); | |||
return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />; | |||
} |
@@ -42,7 +42,6 @@ import { | |||
ComponentMeasureEnhanced, | |||
ComponentMeasureIntern, | |||
Dict, | |||
Issue, | |||
Measure, | |||
Metric, | |||
Paging, | |||
@@ -62,7 +61,6 @@ interface Props { | |||
leakPeriod?: Period; | |||
requestedMetric: Pick<Metric, 'key' | 'direction'>; | |||
metrics: Dict<Metric>; | |||
onIssueChange?: (issue: Issue) => void; | |||
rootComponent: ComponentMeasure; | |||
router: Router; | |||
selected?: string; | |||
@@ -438,7 +436,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
branchLike={branchLike} | |||
component={baseComponent.key} | |||
metricKey={this.state.metric?.key} | |||
onIssueChange={this.props.onIssueChange} | |||
/> | |||
</div> | |||
) : ( |
@@ -30,7 +30,6 @@ import { | |||
ComponentMeasureEnhanced, | |||
ComponentMeasureIntern, | |||
Dict, | |||
Issue, | |||
Metric, | |||
Paging, | |||
Period, | |||
@@ -49,7 +48,6 @@ interface Props { | |||
leakPeriod?: Period; | |||
loading: boolean; | |||
metrics: Dict<Metric>; | |||
onIssueChange?: (issue: Issue) => void; | |||
rootComponent: ComponentMeasure; | |||
updateLoading: (param: Dict<boolean>) => void; | |||
updateSelected: (component: ComponentMeasureIntern) => void; | |||
@@ -127,12 +125,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
if (isFile) { | |||
return ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer | |||
hideHeader | |||
branchLike={branchLike} | |||
component={component.key} | |||
onIssueChange={this.props.onIssueChange} | |||
/> | |||
<SourceViewer hideHeader branchLike={branchLike} component={component.key} /> | |||
</div> | |||
); | |||
} |
@@ -28,7 +28,6 @@ import { | |||
ComponentMeasure, | |||
ComponentMeasureIntern, | |||
Dict, | |||
Issue, | |||
Metric, | |||
Period, | |||
} from '../../../types/types'; | |||
@@ -41,7 +40,6 @@ interface Props { | |||
domain: string; | |||
leakPeriod?: Period; | |||
metrics: Dict<Metric>; | |||
onIssueChange?: (issue: Issue) => void; | |||
rootComponent: ComponentMeasure; | |||
router: Router; | |||
selected?: string; | |||
@@ -135,7 +133,6 @@ export default class MeasureOverviewContainer extends React.PureComponent<Props, | |||
leakPeriod={this.props.leakPeriod} | |||
loading={this.state.loading.component || this.state.loading.bubbles} | |||
metrics={this.props.metrics} | |||
onIssueChange={this.props.onIssueChange} | |||
rootComponent={this.props.rootComponent} | |||
updateLoading={this.updateLoading} | |||
updateSelected={this.updateSelected} |
@@ -42,6 +42,7 @@ import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon'; | |||
import { SEVERITIES } from '../../../helpers/constants'; | |||
import { throwGlobalError } from '../../../helpers/error'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { withBranchStatusRefresh } from '../../../queries/branch'; | |||
import { IssueSeverity } from '../../../types/issues'; | |||
import { Dict, Issue, IssueType, Paging } from '../../../types/types'; | |||
import AssigneeSelect from './AssigneeSelect'; | |||
@@ -51,6 +52,7 @@ interface Props { | |||
fetchIssues: (x: {}) => Promise<{ issues: Issue[]; paging: Paging }>; | |||
onClose: () => void; | |||
onDone: () => void; | |||
refreshBranchStatus: () => void; | |||
} | |||
interface FormFields { | |||
@@ -84,7 +86,7 @@ enum InputField { | |||
export const MAX_PAGE_SIZE = 500; | |||
export default class BulkChangeModal extends React.PureComponent<Props, State> { | |||
export class BulkChangeModal extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
@@ -185,6 +187,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> { | |||
bulkChangeIssues(issueKeys, query).then( | |||
() => { | |||
this.setState({ submitting: false }); | |||
this.props.refreshBranchStatus(); | |||
this.props.onDone(); | |||
}, | |||
(error) => { | |||
@@ -499,3 +502,5 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> { | |||
function hasAction(action: string) { | |||
return (issue: Issue) => issue.actions && issue.actions.includes(action); | |||
} | |||
export default withBranchStatusRefresh(BulkChangeModal); |
@@ -31,13 +31,12 @@ import { | |||
themeBorder, | |||
themeColor, | |||
} from 'design-system'; | |||
import { debounce, keyBy, omit, without } from 'lodash'; | |||
import { keyBy, omit, without } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { getRuleDetails } from '../../../api/rules'; | |||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation'; | |||
@@ -51,12 +50,7 @@ import { Location, Router, withRouter } from '../../../components/hoc/withRouter | |||
import IssueTabViewer from '../../../components/rules/IssueTabViewer'; | |||
import '../../../components/search-navigator.css'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import { | |||
fillBranchLike, | |||
getBranchLikeQuery, | |||
isPullRequest, | |||
isSameBranchLike, | |||
} from '../../../helpers/branch-like'; | |||
import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; | |||
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
import { isDatePicker, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; | |||
@@ -69,6 +63,7 @@ import { | |||
removeWhitePageClass, | |||
} from '../../../helpers/pages'; | |||
import { serializeDate } from '../../../helpers/query'; | |||
import { withBranchLikes } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike, isProject } from '../../../types/component'; | |||
import { | |||
@@ -115,11 +110,9 @@ interface Props { | |||
branchLike?: BranchLike; | |||
component?: Component; | |||
currentUser: CurrentUser; | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; | |||
location: Location; | |||
router: Router; | |||
} | |||
export interface State { | |||
bulkChangeModal: boolean; | |||
cannotShowOpenIssue?: boolean; | |||
@@ -153,7 +146,6 @@ export interface State { | |||
const DEFAULT_QUERY = { resolved: 'false' }; | |||
const MAX_INITAL_FETCH = 1000; | |||
const BRANCH_STATUS_REFRESH_INTERVAL = 1000; | |||
const VARIANTS_FACET = 'codeVariants'; | |||
export class App extends React.PureComponent<Props, State> { | |||
@@ -197,8 +189,6 @@ export class App extends React.PureComponent<Props, State> { | |||
referencedUsers: {}, | |||
selected: getOpen(props.location.query), | |||
}; | |||
this.refreshBranchStatus = debounce(this.refreshBranchStatus, BRANCH_STATUS_REFRESH_INTERVAL); | |||
} | |||
static getDerivedStateFromProps(props: Props, state: State) { | |||
@@ -835,8 +825,6 @@ export class App extends React.PureComponent<Props, State> { | |||
}; | |||
handleIssueChange = (issue: Issue) => { | |||
this.refreshBranchStatus(); | |||
this.setState((state) => ({ | |||
issues: state.issues.map((candidate) => (candidate.key === issue.key ? issue : candidate)), | |||
})); | |||
@@ -856,7 +844,6 @@ export class App extends React.PureComponent<Props, State> { | |||
handleBulkChangeDone = () => { | |||
this.setState({ checkAll: false }); | |||
this.refreshBranchStatus(); | |||
this.fetchFirstIssues(false).catch(() => undefined); | |||
this.handleCloseBulkChange(); | |||
}; | |||
@@ -910,14 +897,6 @@ export class App extends React.PureComponent<Props, State> { | |||
this.setState(actions.selectPreviousFlow); | |||
}; | |||
refreshBranchStatus = () => { | |||
const { branchLike, component } = this.props; | |||
if (branchLike && component && isPullRequest(branchLike)) { | |||
this.props.fetchBranchStatus(branchLike, component.key); | |||
} | |||
}; | |||
renderBulkChange() { | |||
const { currentUser } = this.props; | |||
const { checkAll, bulkChangeModal, checked, issues, paging } = this.state; | |||
@@ -1324,7 +1303,7 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
export default withIndexationGuard( | |||
withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))), | |||
withRouter(withComponentContext(withCurrentUserContext(withBranchLikes(App)))), | |||
PageContext.Issues | |||
); | |||
@@ -26,6 +26,7 @@ import CurrentUserContextProvider from '../../../../app/components/current-user/ | |||
import { SEVERITIES } from '../../../../helpers/constants'; | |||
import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { IssueType } from '../../../../types/issues'; | |||
import { Issue } from '../../../../types/types'; | |||
import { CurrentUser } from '../../../../types/users'; | |||
@@ -187,7 +188,7 @@ it('should properly submit', async () => { | |||
function renderBulkChangeModal( | |||
issues: Issue[], | |||
props: Partial<BulkChangeModal['props']> = {}, | |||
props: Partial<ComponentPropsType<typeof BulkChangeModal>> = {}, | |||
currentUser: CurrentUser = mockLoggedInUser() | |||
) { | |||
return renderComponent( |
@@ -23,7 +23,7 @@ import * as React from 'react'; | |||
import { mockFlowLocation, mockIssue, mockPaging } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { byRole, byText } from '../../../../helpers/testSelector'; | |||
import { FCProps } from '../../../../helpers/testUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { FlowType, Issue } from '../../../../types/types'; | |||
import { VISIBLE_LOCATIONS_COLLAPSE } from '../IssueLocationsCrossFile'; | |||
import SubnavigationIssuesList from '../SubnavigationIssuesList'; | |||
@@ -245,7 +245,7 @@ function getPageObject() { | |||
function renderConciseIssues( | |||
issues: Issue[], | |||
listProps: Partial<FCProps<typeof SubnavigationIssuesList>> = {} | |||
listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {} | |||
) { | |||
const wrapper = renderComponent( | |||
<SubnavigationIssuesList | |||
@@ -266,7 +266,7 @@ function renderConciseIssues( | |||
function override( | |||
issues: Issue[], | |||
listProps: Partial<FCProps<typeof SubnavigationIssuesList>> = {} | |||
listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {} | |||
) { | |||
wrapper.rerender( | |||
<SubnavigationIssuesList |
@@ -166,7 +166,6 @@ export class SidebarClass extends React.PureComponent<Props> { | |||
const displayPeriodFilter = component !== undefined && !isPortfolioLike(component.qualifier); | |||
const displayProjectsFacet = !component || isView(component.qualifier); | |||
const displayAuthorFacet = !component || component.qualifier !== ComponentQualifier.Developper; | |||
return ( | |||
<> | |||
@@ -356,7 +355,7 @@ export class SidebarClass extends React.PureComponent<Props> { | |||
</> | |||
)} | |||
{displayAuthorFacet && !disableDeveloperAggregatedInfo && ( | |||
{!disableDeveloperAggregatedInfo && ( | |||
<> | |||
<BasicSeparator className="sw-my-4" /> | |||
@@ -97,6 +97,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { | |||
this.loadHistory(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.branch !== this.props.branch) { | |||
this.loadStatus(); | |||
this.loadHistory(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} |
@@ -26,8 +26,8 @@ import withComponentContext from '../../../app/components/componentContext/withC | |||
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isPortfolioLike } from '../../../types/component'; | |||
import { Feature } from '../../../types/features'; | |||
import { Component } from '../../../types/types'; | |||
@@ -36,8 +36,6 @@ import PullRequestOverview from '../pullRequests/PullRequestOverview'; | |||
import EmptyOverview from './EmptyOverview'; | |||
interface AppProps extends WithAvailableFeaturesProps { | |||
branchLike?: BranchLike; | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
isInProgress?: boolean; | |||
isPending?: boolean; | |||
@@ -45,13 +43,16 @@ interface AppProps extends WithAvailableFeaturesProps { | |||
} | |||
export function App(props: AppProps) { | |||
const { branchLike, branchLikes, component, projectBinding, isPending, isInProgress } = props; | |||
const { component, projectBinding, isPending, isInProgress } = props; | |||
const branchSupportEnabled = props.hasFeature(Feature.BranchSupport); | |||
const { data } = useBranchesQuery(component); | |||
if (isPortfolioLike(component.qualifier)) { | |||
if (isPortfolioLike(component.qualifier) || !data) { | |||
return null; | |||
} | |||
const { branchLike, branchLikes } = data; | |||
return ( | |||
<> | |||
<Helmet defer={false} title={translate('overview.page')} /> |
@@ -19,14 +19,21 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; | |||
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; | |||
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockCurrentUser } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { App } from '../App'; | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => { | |||
handler.reset(); | |||
}); | |||
it('should render Empty Overview for Application with no analysis', async () => { | |||
renderApp({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }); | |||
@@ -37,7 +44,7 @@ it('should render Empty Overview on main branch with no analysis', async () => { | |||
renderApp({}, mockCurrentUser()); | |||
expect( | |||
await screen.findByText('provisioning.no_analysis_on_main_branch.master') | |||
await screen.findByText('provisioning.no_analysis_on_main_branch.main') | |||
).toBeInTheDocument(); | |||
}); | |||
@@ -46,7 +53,7 @@ it('should render Empty Overview on main branch with multiple branches with bad | |||
expect( | |||
await screen.findByText( | |||
'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch' | |||
'provisioning.no_analysis_on_main_branch.bad_configuration.main.branches.main_branch' | |||
) | |||
).toBeInTheDocument(); | |||
}); | |||
@@ -68,13 +75,8 @@ it('should not render for portfolios and subportfolios', () => { | |||
function renderApp(props = {}, userProps = {}) { | |||
return renderComponent( | |||
<CurrentUserContextProvider currentUser={mockCurrentUser({ isLoggedIn: true, ...userProps })}> | |||
<App | |||
hasFeature={jest.fn().mockReturnValue(false)} | |||
branchLikes={[]} | |||
branchLike={mockMainBranch()} | |||
component={mockComponent()} | |||
{...props} | |||
/> | |||
</CurrentUserContextProvider> | |||
<App hasFeature={jest.fn().mockReturnValue(false)} component={mockComponent()} {...props} /> | |||
</CurrentUserContextProvider>, | |||
'/?id=my-project' | |||
); | |||
} |
@@ -29,21 +29,20 @@ import { | |||
PageTitle, | |||
TextMuted, | |||
} from 'design-system'; | |||
import { differenceBy, uniq } from 'lodash'; | |||
import { uniq } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useEffect, useState } from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { getMeasuresWithMetrics } from '../../../api/measures'; | |||
import { BranchStatusContextInterface } from '../../../app/components/branch-status/BranchStatusContext'; | |||
import withBranchStatus from '../../../app/components/branch-status/withBranchStatus'; | |||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import { duplicationRatingConverter } from '../../../components/measure/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls'; | |||
import { BranchStatusData, PullRequest } from '../../../types/branch-like'; | |||
import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls'; | |||
import { useBranchStatusQuery } from '../../../queries/branch'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { IssueType } from '../../../types/issues'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure'; | |||
@@ -57,73 +56,21 @@ import SonarLintPromotion from '../components/SonarLintPromotion'; | |||
import '../styles.css'; | |||
import { MeasurementType, PR_METRICS } from '../utils'; | |||
interface Props extends BranchStatusData, Pick<BranchStatusContextInterface, 'fetchBranchStatus'> { | |||
interface Props { | |||
branchLike: PullRequest; | |||
component: Component; | |||
} | |||
interface State { | |||
loading: boolean; | |||
measures: MeasureEnhanced[]; | |||
} | |||
export class PullRequestOverview extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
loading: false, | |||
measures: [], | |||
}; | |||
export default function PullRequestOverview(props: Props) { | |||
const { component, branchLike } = props; | |||
const [loadingMeasure, setLoadingMeasure] = useState(false); | |||
const [measures, setMeasures] = useState<MeasureEnhanced[]>([]); | |||
const { data: { conditions, ignoredConditions, status } = {}, isLoading } = | |||
useBranchStatusQuery(component); | |||
const loading = isLoading || loadingMeasure; | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (this.props.conditions === undefined) { | |||
this.fetchBranchStatusData(); | |||
} else { | |||
this.fetchBranchData(); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.conditionsHaveChanged(prevProps)) { | |||
this.fetchBranchData(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
conditionsHaveChanged = (prevProps: Props) => { | |||
const prevConditions = prevProps.conditions ?? []; | |||
const newConditions = this.props.conditions ?? []; | |||
const diff = differenceBy( | |||
prevConditions.filter((c) => c.level === 'ERROR'), | |||
newConditions.filter((c) => c.level === 'ERROR'), | |||
(c) => c.metric | |||
); | |||
return ( | |||
(prevProps.conditions === undefined && this.props.conditions !== undefined) || diff.length > 0 | |||
); | |||
}; | |||
fetchBranchStatusData = () => { | |||
const { | |||
branchLike, | |||
component: { key }, | |||
} = this.props; | |||
this.props.fetchBranchStatus(branchLike, key); | |||
}; | |||
fetchBranchData = () => { | |||
const { | |||
branchLike, | |||
component: { key }, | |||
conditions, | |||
} = this.props; | |||
this.setState({ loading: true }); | |||
useEffect(() => { | |||
setLoadingMeasure(true); | |||
const metricKeys = | |||
conditions !== undefined | |||
@@ -131,153 +78,140 @@ export class PullRequestOverview extends React.PureComponent<Props, State> { | |||
uniq([...PR_METRICS, ...conditions.filter((c) => c.level !== 'OK').map((c) => c.metric)]) | |||
: PR_METRICS; | |||
getMeasuresWithMetrics(key, metricKeys, getBranchLikeQuery(branchLike)).then( | |||
getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then( | |||
({ component, metrics }) => { | |||
if (this.mounted && component.measures) { | |||
this.setState({ | |||
loading: false, | |||
measures: enhanceMeasuresWithMetrics(component.measures || [], metrics), | |||
}); | |||
if (component.measures) { | |||
setLoadingMeasure(false); | |||
setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics)); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
setLoadingMeasure(false); | |||
} | |||
); | |||
}; | |||
}, [branchLike, component.key, conditions]); | |||
if (loading) { | |||
return ( | |||
<LargeCenteredLayout> | |||
<div className="sw-p-6"> | |||
<DeferredSpinner loading /> | |||
</div> | |||
</LargeCenteredLayout> | |||
); | |||
} | |||
render() { | |||
const { branchLike, component, conditions, ignoredConditions, status } = this.props; | |||
const { loading, measures } = this.state; | |||
if (conditions === undefined) { | |||
return null; | |||
} | |||
if (loading) { | |||
return ( | |||
<LargeCenteredLayout> | |||
<div className="sw-p-6"> | |||
<DeferredSpinner loading /> | |||
</div> | |||
</LargeCenteredLayout> | |||
); | |||
} | |||
const path = | |||
component.qualityGate === undefined | |||
? getQualityGatesUrl() | |||
: getQualityGateUrl(component.qualityGate.name); | |||
const failedConditions = conditions | |||
.filter((condition) => condition.level === 'ERROR') | |||
.map((c) => enhanceConditionWithMeasure(c, measures)) | |||
.filter(isDefined); | |||
return ( | |||
<LargeCenteredLayout> | |||
<div className="it__pr-overview sw-mt-12"> | |||
<div className="sw-flex"> | |||
<div className="sw-flex sw-flex-col sw-mr-12 width-30"> | |||
<QualityGateStatusTitle /> | |||
<Card> | |||
{status && ( | |||
<QualityGateStatusHeader | |||
status={status} | |||
failedConditionCount={failedConditions.length} | |||
/> | |||
)} | |||
<div className="sw-flex sw-items-center sw-mb-4"> | |||
<TextMuted text={translate('overview.on_new_code_long')} /> | |||
<HelpTooltip | |||
className="sw-ml-2" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage={translate('overview.quality_gate.conditions_on_new_code')} | |||
id="overview.quality_gate.conditions_on_new_code" | |||
values={{ | |||
link: <Link to={path}>{translate('overview.quality_gate')}</Link>, | |||
}} | |||
/> | |||
} | |||
> | |||
<HelperHintIcon aria-label="help-tooltip" /> | |||
</HelpTooltip> | |||
</div> | |||
if (conditions === undefined) { | |||
return null; | |||
} | |||
{ignoredConditions && <IgnoredConditionWarning />} | |||
const path = | |||
component.qualityGate === undefined | |||
? getQualityGatesUrl() | |||
: getQualityGateUrl(component.qualityGate.name); | |||
{status === 'OK' && failedConditions.length === 0 && <QualityGateStatusPassedView />} | |||
const failedConditions = conditions | |||
.filter((condition) => condition.level === 'ERROR') | |||
.map((c) => enhanceConditionWithMeasure(c, measures)) | |||
.filter(isDefined); | |||
{status !== 'OK' && <BasicSeparator />} | |||
return ( | |||
<LargeCenteredLayout> | |||
<div className="it__pr-overview sw-mt-12"> | |||
<div className="sw-flex"> | |||
<div className="sw-flex sw-flex-col sw-mr-12 width-30"> | |||
<QualityGateStatusTitle /> | |||
<Card> | |||
{status && ( | |||
<QualityGateStatusHeader | |||
status={status} | |||
failedConditionCount={failedConditions.length} | |||
{failedConditions.length > 0 && ( | |||
<div> | |||
<QualityGateConditions | |||
branchLike={branchLike} | |||
collapsible | |||
component={component} | |||
failedConditions={failedConditions} | |||
/> | |||
)} | |||
<div className="sw-flex sw-items-center sw-mb-4"> | |||
<TextMuted text={translate('overview.on_new_code_long')} /> | |||
<HelpTooltip | |||
className="sw-ml-2" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage={translate('overview.quality_gate.conditions_on_new_code')} | |||
id="overview.quality_gate.conditions_on_new_code" | |||
values={{ | |||
link: <Link to={path}>{translate('overview.quality_gate')}</Link>, | |||
}} | |||
/> | |||
} | |||
> | |||
<HelperHintIcon aria-label="help-tooltip" /> | |||
</HelpTooltip> | |||
</div> | |||
)} | |||
</Card> | |||
<SonarLintPromotion qgConditions={conditions} /> | |||
</div> | |||
{ignoredConditions && <IgnoredConditionWarning />} | |||
{status === 'OK' && failedConditions.length === 0 && ( | |||
<QualityGateStatusPassedView /> | |||
)} | |||
{status !== 'OK' && <BasicSeparator />} | |||
{failedConditions.length > 0 && ( | |||
<div> | |||
<QualityGateConditions | |||
branchLike={branchLike} | |||
collapsible | |||
component={component} | |||
failedConditions={failedConditions} | |||
/> | |||
</div> | |||
)} | |||
</Card> | |||
<SonarLintPromotion qgConditions={conditions} /> | |||
<div className="sw-flex-1"> | |||
<div className="sw-body-md-highlight"> | |||
<PageTitle as="h2" text={translate('overview.measures')} /> | |||
</div> | |||
<div className="sw-flex-1"> | |||
<div className="sw-body-md-highlight"> | |||
<PageTitle as="h2" text={translate('overview.measures')} /> | |||
</div> | |||
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> | |||
{[ | |||
IssueType.Bug, | |||
IssueType.Vulnerability, | |||
IssueType.SecurityHotspot, | |||
IssueType.CodeSmell, | |||
].map((type: IssueType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelIssueMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
isNewCodeTab | |||
measures={measures} | |||
type={type} | |||
/> | |||
</Card> | |||
))} | |||
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> | |||
{[ | |||
IssueType.Bug, | |||
IssueType.Vulnerability, | |||
IssueType.SecurityHotspot, | |||
IssueType.CodeSmell, | |||
].map((type: IssueType) => ( | |||
{[MeasurementType.Coverage, MeasurementType.Duplication].map( | |||
(type: MeasurementType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelIssueMeasure | |||
<MeasuresPanelPercentMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
isNewCodeTab | |||
measures={measures} | |||
ratingIcon={renderMeasureIcon(type)} | |||
type={type} | |||
useDiffMetric | |||
/> | |||
</Card> | |||
))} | |||
{[MeasurementType.Coverage, MeasurementType.Duplication].map( | |||
(type: MeasurementType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelPercentMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
ratingIcon={renderMeasureIcon(type)} | |||
type={type} | |||
useDiffMetric | |||
/> | |||
</Card> | |||
) | |||
)} | |||
</div> | |||
) | |||
)} | |||
</div> | |||
</div> | |||
</div> | |||
</LargeCenteredLayout> | |||
); | |||
} | |||
</div> | |||
</LargeCenteredLayout> | |||
); | |||
} | |||
export default withBranchStatus(withBranchStatusActions(PullRequestOverview)); | |||
function renderMeasureIcon(type: MeasurementType) { | |||
if (type === MeasurementType.Coverage) { | |||
return function CoverageIndicatorRenderer(value?: string) { |
@@ -17,17 +17,23 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import { screen, waitFor } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { getQualityGateProjectStatus } from '../../../../api/quality-gates'; | |||
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; | |||
import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockQualityGateStatusCondition } from '../../../../helpers/mocks/quality-gates'; | |||
import { | |||
mockQualityGateProjectCondition, | |||
mockQualityGateStatusCondition, | |||
} from '../../../../helpers/mocks/quality-gates'; | |||
import { mockLoggedInUser, mockMetric, mockPeriod } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { PullRequestOverview } from '../PullRequestOverview'; | |||
import { CaycStatus } from '../../../../types/types'; | |||
import PullRequestOverview from '../PullRequestOverview'; | |||
jest.mock('../../../../api/measures', () => { | |||
return { | |||
@@ -112,40 +118,59 @@ jest.mock('../../../../api/quality-gates', () => { | |||
}); | |||
it('should render correctly for a passed QG', async () => { | |||
renderPullRequestOverview({ status: 'OK', conditions: [] }); | |||
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({ | |||
status: 'OK', | |||
conditions: [], | |||
caycStatus: CaycStatus.Compliant, | |||
ignoredConditions: false, | |||
}); | |||
renderPullRequestOverview(); | |||
expect(await screen.findByText('metric.level.OK')).toBeInTheDocument(); | |||
await waitFor(async () => expect(await screen.findByText('metric.level.OK')).toBeInTheDocument()); | |||
}); | |||
it('should render correctly if conditions are ignored', async () => { | |||
renderPullRequestOverview({ conditions: [], ignoredConditions: true }); | |||
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({ | |||
status: 'OK', | |||
conditions: [], | |||
caycStatus: CaycStatus.Compliant, | |||
ignoredConditions: true, | |||
}); | |||
renderPullRequestOverview(); | |||
expect(await screen.findByText('overview.quality_gate.ignored_conditions')).toBeInTheDocument(); | |||
await waitFor(async () => | |||
expect(await screen.findByText('overview.quality_gate.ignored_conditions')).toBeInTheDocument() | |||
); | |||
}); | |||
it('should render correctly for a failed QG', async () => { | |||
renderPullRequestOverview({ | |||
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({ | |||
status: 'ERROR', | |||
conditions: [ | |||
mockQualityGateStatusCondition({ | |||
error: '2.0', | |||
metric: MetricKey.new_coverage, | |||
period: 1, | |||
mockQualityGateProjectCondition({ | |||
errorThreshold: '2.0', | |||
metricKey: MetricKey.new_coverage, | |||
periodIndex: 1, | |||
}), | |||
mockQualityGateStatusCondition({ | |||
error: '1.0', | |||
metric: MetricKey.duplicated_lines, | |||
period: 1, | |||
mockQualityGateProjectCondition({ | |||
errorThreshold: '1.0', | |||
metricKey: MetricKey.duplicated_lines, | |||
periodIndex: 1, | |||
}), | |||
mockQualityGateStatusCondition({ | |||
error: '3', | |||
metric: MetricKey.new_bugs, | |||
period: 1, | |||
mockQualityGateProjectCondition({ | |||
errorThreshold: '3', | |||
metricKey: MetricKey.new_bugs, | |||
periodIndex: 1, | |||
}), | |||
], | |||
caycStatus: CaycStatus.Compliant, | |||
ignoredConditions: true, | |||
}); | |||
renderPullRequestOverview(); | |||
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); | |||
await waitFor(async () => | |||
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument() | |||
); | |||
expect(await screen.findByText('metric.new_coverage.name')).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument(); | |||
@@ -158,11 +183,12 @@ it('should render correctly for a failed QG', async () => { | |||
expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument(); | |||
}); | |||
function renderPullRequestOverview(props: Partial<PullRequestOverview['props']> = {}) { | |||
function renderPullRequestOverview( | |||
props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {} | |||
) { | |||
renderComponent( | |||
<CurrentUserContextProvider currentUser={mockLoggedInUser()}> | |||
<PullRequestOverview | |||
fetchBranchStatus={jest.fn()} | |||
branchLike={mockPullRequest()} | |||
component={mockComponent({ | |||
breadcrumbs: [mockComponent({ key: 'foo' })], |
@@ -54,6 +54,12 @@ export default class BranchList extends React.PureComponent<Props, State> { | |||
this.fetchBranches(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.branchList !== this.props.branchList) { | |||
this.fetchBranches(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} |
@@ -36,6 +36,7 @@ import { | |||
DEFAULT_NEW_CODE_DEFINITION_TYPE, | |||
getNumberOfDaysDefaultValue, | |||
} from '../../../helpers/new-code-definition'; | |||
import { withBranchLikes } from '../../../queries/branch'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Branch, BranchLike } from '../../../types/branch-like'; | |||
import { Feature } from '../../../types/features'; | |||
@@ -130,7 +131,7 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> { | |||
sortAndFilterBranches(branchLikes: BranchLike[] = []) { | |||
const branchList = sortBranches(branchLikes.filter(isBranch)); | |||
this.setState({ branchList, referenceBranch: branchList[0].name }); | |||
this.setState({ branchList, referenceBranch: branchList[0]?.name }); | |||
} | |||
fetchLeakPeriodSetting() { | |||
@@ -141,7 +142,7 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> { | |||
Promise.all([ | |||
getNewCodePeriod(), | |||
getNewCodePeriod({ | |||
branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike.name, | |||
branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name, | |||
project: component.key, | |||
}), | |||
]).then( | |||
@@ -344,4 +345,6 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
export default withComponentContext(withAvailableFeatures(withAppStateContext(ProjectBaselineApp))); | |||
export default withComponentContext( | |||
withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectBaselineApp))) | |||
); |
@@ -40,7 +40,7 @@ import BranchAnalysisList from './BranchAnalysisList'; | |||
export interface ProjectBaselineSelectorProps { | |||
analysis?: string; | |||
branch: Branch; | |||
branch?: Branch; | |||
branchList: Branch[]; | |||
branchesEnabled?: boolean; | |||
canAdmin: boolean | undefined; | |||
@@ -94,6 +94,10 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr | |||
selected, | |||
}); | |||
if (branch === undefined) { | |||
return null; | |||
} | |||
return ( | |||
<form className="project-baseline-selector" onSubmit={props.onSubmit}> | |||
<div className="big-spacer-top spacer-bottom" role="radiogroup"> |
@@ -21,9 +21,9 @@ import { within } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { first, last } from 'lodash'; | |||
import selectEvent from 'react-select-event'; | |||
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; | |||
import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock'; | |||
import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock'; | |||
import { mockBranch } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
@@ -38,11 +38,14 @@ import routes from '../../routes'; | |||
jest.mock('../../../../api/newCodePeriod'); | |||
jest.mock('../../../../api/projectActivity'); | |||
jest.mock('../../../../api/branches'); | |||
const codePeriodsMock = new NewCodePeriodsServiceMock(); | |||
const projectActivityMock = new ProjectActivityServiceMock(); | |||
const branchHandler = new BranchesServiceMock(); | |||
afterEach(() => { | |||
branchHandler.reset(); | |||
codePeriodsMock.reset(); | |||
projectActivityMock.reset(); | |||
}); | |||
@@ -52,7 +55,7 @@ it('renders correctly without branch support feature', async () => { | |||
renderProjectBaselineApp(); | |||
await ui.appIsLoaded(); | |||
expect(ui.generalSettingRadio.get()).toBeChecked(); | |||
expect(await ui.generalSettingRadio.find()).toBeChecked(); | |||
expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument(); | |||
// User is not admin | |||
@@ -74,7 +77,7 @@ it('prevents selection of global setting if it is not compliant and warns non-ad | |||
renderProjectBaselineApp(); | |||
await ui.appIsLoaded(); | |||
expect(ui.generalSettingRadio.get()).toBeChecked(); | |||
expect(await ui.generalSettingRadio.find()).toBeChecked(); | |||
expect(ui.generalSettingRadio.get()).toBeDisabled(); | |||
expect(ui.complianceWarning.get()).toBeVisible(); | |||
}); | |||
@@ -90,7 +93,7 @@ it('prevents selection of global setting if it is not compliant and warns admin | |||
renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) }); | |||
await ui.appIsLoaded(); | |||
expect(ui.generalSettingRadio.get()).toBeChecked(); | |||
expect(await ui.generalSettingRadio.find()).toBeChecked(); | |||
expect(ui.generalSettingRadio.get()).toBeDisabled(); | |||
expect(ui.complianceWarningAdmin.get()).toBeVisible(); | |||
expect(ui.complianceWarning.query()).not.toBeInTheDocument(); | |||
@@ -104,7 +107,7 @@ it('renders correctly with branch support feature', async () => { | |||
}); | |||
await ui.appIsLoaded(); | |||
expect(ui.generalSettingRadio.get()).toBeChecked(); | |||
expect(await ui.generalSettingRadio.find()).toBeChecked(); | |||
expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument(); | |||
// User is admin | |||
@@ -120,7 +123,7 @@ it('can set previous version specific setting', async () => { | |||
renderProjectBaselineApp(); | |||
await ui.appIsLoaded(); | |||
expect(ui.previousVersionRadio.get()).toHaveClass('disabled'); | |||
expect(await ui.previousVersionRadio.find()).toHaveClass('disabled'); | |||
await ui.setPreviousVersionSetting(); | |||
expect(ui.previousVersionRadio.get()).toBeChecked(); | |||
@@ -141,7 +144,7 @@ it('can set number of days specific setting', async () => { | |||
renderProjectBaselineApp(); | |||
await ui.appIsLoaded(); | |||
expect(ui.numberDaysRadio.get()).toHaveClass('disabled'); | |||
expect(await ui.numberDaysRadio.find()).toHaveClass('disabled'); | |||
await ui.setNumberDaysSetting('10'); | |||
expect(ui.numberDaysRadio.get()).toBeChecked(); | |||
@@ -164,7 +167,7 @@ it('can set reference branch specific setting', async () => { | |||
}); | |||
await ui.appIsLoaded(); | |||
expect(ui.referenceBranchRadio.get()).toHaveClass('disabled'); | |||
expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled'); | |||
await ui.setReferenceBranchSetting('main'); | |||
expect(ui.referenceBranchRadio.get()).toBeChecked(); | |||
@@ -183,7 +186,7 @@ it('cannot set specific analysis setting', async () => { | |||
renderProjectBaselineApp(); | |||
await ui.appIsLoaded(); | |||
expect(ui.specificAnalysisRadio.get()).toBeChecked(); | |||
expect(await ui.specificAnalysisRadio.find()).toBeChecked(); | |||
expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled'); | |||
expect(ui.specificAnalysisWarning.get()).toBeInTheDocument(); | |||
@@ -274,18 +277,25 @@ it('can set a reference branch setting for branch', async () => { | |||
}); | |||
await ui.appIsLoaded(); | |||
await ui.setBranchReferenceToBranchSetting('main', 'feature'); | |||
await ui.setBranchReferenceToBranchSetting('main', 'normal-branch'); | |||
expect(byRole('table').byText('baseline.reference_branch: feature').get()).toBeInTheDocument(); | |||
expect( | |||
byRole('table').byText('baseline.reference_branch: normal-branch').get() | |||
).toBeInTheDocument(); | |||
}); | |||
function renderProjectBaselineApp(context: RenderContext = {}) { | |||
const branch = mockBranch({ name: 'main', isMain: true }); | |||
return renderAppWithComponentContext('baseline', routes, context, { | |||
component: mockComponent(), | |||
branchLike: branch, | |||
branchLikes: [branch, mockBranch({ name: 'feature' })], | |||
}); | |||
function renderProjectBaselineApp(context: RenderContext = {}, params?: string) { | |||
return renderAppWithComponentContext( | |||
'baseline', | |||
routes, | |||
{ | |||
...context, | |||
navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project', | |||
}, | |||
{ | |||
component: mockComponent(), | |||
} | |||
); | |||
} | |||
function getPageObjects() { | |||
@@ -293,6 +303,7 @@ function getPageObjects() { | |||
const ui = { | |||
pageHeading: byRole('heading', { name: 'project_baseline.page' }), | |||
branchTableHeading: byText('branch_list.branch'), | |||
branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }), | |||
generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }), | |||
generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }), |
@@ -21,19 +21,16 @@ import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import withComponentContext from '../../app/components/componentContext/withComponentContext'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { Component } from '../../types/types'; | |||
import BranchLikeTabs from './components/BranchLikeTabs'; | |||
import LifetimeInformation from './components/LifetimeInformation'; | |||
export interface ProjectBranchesAppProps { | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
onBranchesChange: () => void; | |||
} | |||
function ProjectBranchesApp(props: ProjectBranchesAppProps) { | |||
const { branchLikes, component } = props; | |||
const { component } = props; | |||
return ( | |||
<div className="page page-limited" id="project-branch-like"> | |||
@@ -43,11 +40,7 @@ function ProjectBranchesApp(props: ProjectBranchesAppProps) { | |||
<LifetimeInformation /> | |||
</header> | |||
<BranchLikeTabs | |||
branchLikes={branchLikes} | |||
component={component} | |||
onBranchesChange={props.onBranchesChange} | |||
/> | |||
<BranchLikeTabs component={component} /> | |||
</div> | |||
); | |||
} |
@@ -20,56 +20,61 @@ | |||
import { act, within } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import React, { useEffect, useState } from 'react'; | |||
import React from 'react'; | |||
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; | |||
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; | |||
import BranchStatusContextProvider from '../../../app/components/branch-status/BranchStatusContextProvider'; | |||
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; | |||
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockAppState } from '../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { byRole } from '../../../helpers/testSelector'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Feature } from '../../../types/features'; | |||
import { SettingsKey } from '../../../types/settings'; | |||
import ProjectBranchesApp from '../ProjectBranchesApp'; | |||
const handler = new BranchesServiceMock(); | |||
const settingsHandler = new SettingsServiceMock(); | |||
const ui = { | |||
branchTabContent: byRole('tabpanel', { name: 'project_branch_pull_request.tabs.branches' }), | |||
branchTabBtn: byRole('tab', { name: 'project_branch_pull_request.tabs.branches' }), | |||
linkForAdmin: byRole('link', { name: 'settings.page' }), | |||
renameBranchBtn: byRole('button', { name: 'project_branch_pull_request.branch.rename' }), | |||
deleteBranchBtn: byRole('button', { name: 'project_branch_pull_request.branch.delete' }), | |||
deletePullRequestBtn: byRole('button', { | |||
const ui = new (class UI { | |||
branchTabContent = byRole('tabpanel', { name: 'project_branch_pull_request.tabs.branches' }); | |||
branchTabBtn = byRole('tab', { name: 'project_branch_pull_request.tabs.branches' }); | |||
linkForAdmin = byRole('link', { name: 'settings.page' }); | |||
renameBranchBtn = byRole('button', { name: 'project_branch_pull_request.branch.rename' }); | |||
deleteBranchBtn = byRole('button', { name: 'project_branch_pull_request.branch.delete' }); | |||
deletePullRequestBtn = byRole('button', { | |||
name: 'project_branch_pull_request.pull_request.delete', | |||
}), | |||
pullRequestTabContent: byRole('tabpanel', { | |||
}); | |||
pullRequestTabContent = byRole('tabpanel', { | |||
name: 'project_branch_pull_request.tabs.pull_requests', | |||
}), | |||
pullRequestTabBtn: byRole('tab', { | |||
}); | |||
pullRequestTabBtn = byRole('tab', { | |||
name: 'project_branch_pull_request.tabs.pull_requests', | |||
}), | |||
renameBranchDialog: byRole('dialog', { name: 'project_branch_pull_request.branch.rename' }), | |||
deleteBranchDialog: byRole('dialog', { name: 'project_branch_pull_request.branch.delete' }), | |||
deletePullRequestDialog: byRole('dialog', { | |||
}); | |||
renameBranchDialog = byRole('dialog', { name: 'project_branch_pull_request.branch.rename' }); | |||
deleteBranchDialog = byRole('dialog', { name: 'project_branch_pull_request.branch.delete' }); | |||
deletePullRequestDialog = byRole('dialog', { | |||
name: 'project_branch_pull_request.pull_request.delete', | |||
}), | |||
updateMasterBtn: byRole('button', { | |||
name: 'project_branch_pull_request.branch.actions_label.master', | |||
}), | |||
updateSecondBranchBtn: byRole('button', { | |||
}); | |||
updateMasterBtn = byRole('button', { | |||
name: 'project_branch_pull_request.branch.actions_label.main', | |||
}); | |||
updateSecondBranchBtn = byRole('button', { | |||
name: 'project_branch_pull_request.branch.actions_label.delete-branch', | |||
}), | |||
updateFirstPRBtn: byRole('button', { | |||
}); | |||
updateFirstPRBtn = byRole('button', { | |||
name: 'project_branch_pull_request.branch.actions_label.01 – TEST-191 update master', | |||
}), | |||
getBranchRow: () => within(ui.branchTabContent.get()).getAllByRole('row'), | |||
getPullRequestRow: () => within(ui.pullRequestTabContent.get()).getAllByRole('row'), | |||
}; | |||
}); | |||
branchRow = this.branchTabContent.byRole('row'); | |||
pullRequestRow = this.pullRequestTabContent.byRole('row'); | |||
})(); | |||
beforeEach(() => { | |||
jest.useFakeTimers({ | |||
@@ -90,13 +95,13 @@ it('should show all branches', async () => { | |||
expect(await ui.branchTabContent.find()).toBeInTheDocument(); | |||
expect(ui.pullRequestTabContent.query()).not.toBeInTheDocument(); | |||
expect(ui.linkForAdmin.query()).not.toBeInTheDocument(); | |||
expect(ui.getBranchRow()).toHaveLength(4); | |||
expect(ui.getBranchRow()[1]).toHaveTextContent('masterbranches.main_branchOK1 month ago'); | |||
expect(within(ui.getBranchRow()[1]).getByRole('switch')).toBeDisabled(); | |||
expect(within(ui.getBranchRow()[1]).getByRole('switch')).toBeChecked(); | |||
expect(ui.getBranchRow()[2]).toHaveTextContent('delete-branchERROR2 days ago'); | |||
expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeEnabled(); | |||
expect(within(ui.getBranchRow()[2]).getByRole('switch')).not.toBeChecked(); | |||
expect(await ui.branchRow.findAll()).toHaveLength(4); | |||
expect(ui.branchRow.getAt(1)).toHaveTextContent('mainbranches.main_branchOK1 month ago'); | |||
expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeDisabled(); | |||
expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeChecked(); | |||
expect(ui.branchRow.getAt(2)).toHaveTextContent('delete-branchERROR2 days ago'); | |||
expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeEnabled(); | |||
expect(within(ui.branchRow.getAt(2)).getByRole('switch')).not.toBeChecked(); | |||
}); | |||
it('should show link to change purge options for admin', async () => { | |||
@@ -112,7 +117,7 @@ it('should be able to rename main branch, but not others', async () => { | |||
expect(ui.renameBranchBtn.get()).toBeInTheDocument(); | |||
await user.click(ui.renameBranchBtn.get()); | |||
expect(ui.renameBranchDialog.get()).toBeInTheDocument(); | |||
expect(within(ui.renameBranchDialog.get()).getByRole('textbox')).toHaveValue('master'); | |||
expect(within(ui.renameBranchDialog.get()).getByRole('textbox')).toHaveValue('main'); | |||
expect( | |||
within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' }) | |||
).toBeDisabled(); | |||
@@ -120,12 +125,12 @@ it('should be able to rename main branch, but not others', async () => { | |||
expect( | |||
within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' }) | |||
).toBeDisabled(); | |||
await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'main'); | |||
await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'master'); | |||
expect(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })).toBeEnabled(); | |||
await act(() => | |||
user.click(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })) | |||
); | |||
expect(ui.getBranchRow()[1]).toHaveTextContent('mainbranches.main_branchOK1 month ago'); | |||
expect(ui.branchRow.getAt(1)).toHaveTextContent('masterbranches.main_branchOK1 month ago'); | |||
await user.click(await ui.updateSecondBranchBtn.find()); | |||
expect(ui.renameBranchBtn.query()).not.toBeInTheDocument(); | |||
@@ -142,7 +147,7 @@ it('should be able to delete branch, but not main', async () => { | |||
await act(() => | |||
user.click(within(ui.deleteBranchDialog.get()).getByRole('button', { name: 'delete' })) | |||
); | |||
expect(ui.getBranchRow()).toHaveLength(3); | |||
expect(ui.branchRow.getAll()).toHaveLength(3); | |||
await user.click(await ui.updateMasterBtn.find()); | |||
expect(ui.deleteBranchBtn.query()).not.toBeInTheDocument(); | |||
@@ -152,18 +157,18 @@ it('should exclude from purge', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectBranchesApp(); | |||
expect(await ui.branchTabContent.find()).toBeInTheDocument(); | |||
expect(within(ui.getBranchRow()[2]).getByRole('switch')).not.toBeChecked(); | |||
await act(() => user.click(within(ui.getBranchRow()[2]).getByRole('switch'))); | |||
expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeChecked(); | |||
expect(within(await ui.branchRow.findAt(2)).getByRole('switch')).not.toBeChecked(); | |||
await act(() => user.click(within(ui.branchRow.getAt(2)).getByRole('switch'))); | |||
expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeChecked(); | |||
expect(within(ui.getBranchRow()[3]).getByRole('switch')).toBeChecked(); | |||
await act(() => user.click(within(ui.getBranchRow()[3]).getByRole('switch'))); | |||
expect(within(ui.getBranchRow()[3]).getByRole('switch')).not.toBeChecked(); | |||
expect(within(ui.branchRow.getAt(3)).getByRole('switch')).toBeChecked(); | |||
await act(() => user.click(within(ui.branchRow.getAt(3)).getByRole('switch'))); | |||
expect(within(ui.branchRow.getAt(3)).getByRole('switch')).not.toBeChecked(); | |||
await user.click(ui.pullRequestTabBtn.get()); | |||
await user.click(ui.branchTabBtn.get()); | |||
expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeChecked(); | |||
expect(within(ui.getBranchRow()[3]).getByRole('switch')).not.toBeChecked(); | |||
expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeChecked(); | |||
expect(within(ui.branchRow.getAt(3)).getByRole('switch')).not.toBeChecked(); | |||
}); | |||
it('should show all pull requests', async () => { | |||
@@ -172,9 +177,9 @@ it('should show all pull requests', async () => { | |||
await user.click(await ui.pullRequestTabBtn.find()); | |||
expect(await ui.pullRequestTabContent.find()).toBeInTheDocument(); | |||
expect(ui.branchTabContent.query()).not.toBeInTheDocument(); | |||
expect(ui.getPullRequestRow()).toHaveLength(4); | |||
expect(ui.getPullRequestRow()[1]).toHaveTextContent('01 – TEST-191 update masterOK1 month ago'); | |||
expect(ui.getPullRequestRow()[2]).toHaveTextContent( | |||
expect(await ui.pullRequestRow.findAll()).toHaveLength(4); | |||
expect(ui.pullRequestRow.getAt(1)).toHaveTextContent('01 – TEST-191 update masterOK1 month ago'); | |||
expect(ui.pullRequestRow.getAt(2)).toHaveTextContent( | |||
'02 – TEST-192 update normal-branchERROR2 days ago' | |||
); | |||
}); | |||
@@ -183,7 +188,7 @@ it('should delete pull requests', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectBranchesApp(); | |||
await user.click(await ui.pullRequestTabBtn.find()); | |||
expect(ui.getPullRequestRow()).toHaveLength(4); | |||
expect(await ui.pullRequestRow.findAll()).toHaveLength(4); | |||
await user.click(ui.updateFirstPRBtn.get()); | |||
await user.click(ui.deletePullRequestBtn.get()); | |||
expect(await ui.deletePullRequestDialog.find()).toBeInTheDocument(); | |||
@@ -191,57 +196,20 @@ it('should delete pull requests', async () => { | |||
await act(() => | |||
user.click(within(ui.deletePullRequestDialog.get()).getByRole('button', { name: 'delete' })) | |||
); | |||
expect(ui.getPullRequestRow()).toHaveLength(3); | |||
expect(ui.pullRequestRow.getAll()).toHaveLength(3); | |||
}); | |||
function renderProjectBranchesApp(overrides?: Partial<AppState>) { | |||
function TestWrapper(props: any) { | |||
const [init, setInit] = useState<boolean>(false); | |||
const [branches, setBranches] = useState<BranchLike[]>([ | |||
...handler.branches, | |||
...handler.pullRequests, | |||
]); | |||
const updateBranches = (branches: BranchLike[]) => { | |||
branches.forEach((item) => { | |||
props.updateBranchStatus(item, 'my-project', item.status?.qualityGateStatus); | |||
}); | |||
}; | |||
useEffect(() => { | |||
updateBranches(branches); | |||
setInit(true); | |||
}, []); | |||
const onBranchesChange = () => { | |||
const changedBranches = [...handler.branches, ...handler.pullRequests]; | |||
updateBranches(changedBranches); | |||
setBranches(changedBranches); | |||
}; | |||
return init ? ( | |||
<ComponentContext.Provider | |||
value={{ | |||
branchLikes: branches, | |||
onBranchesChange, | |||
onComponentChange: jest.fn(), | |||
component: mockComponent(), | |||
}} | |||
> | |||
{props.children} | |||
</ComponentContext.Provider> | |||
) : null; | |||
} | |||
const Wrapper = withBranchStatusActions(TestWrapper); | |||
return renderComponent( | |||
<BranchStatusContextProvider> | |||
<Wrapper> | |||
<ProjectBranchesApp /> | |||
</Wrapper> | |||
</BranchStatusContextProvider>, | |||
'/', | |||
{ appState: mockAppState(overrides) } | |||
<ComponentContext.Provider | |||
value={{ | |||
onComponentChange: jest.fn(), | |||
component: mockComponent(), | |||
}} | |||
> | |||
<ProjectBranchesApp /> | |||
</ComponentContext.Provider>, | |||
'/?id=my-project', | |||
{ appState: mockAppState(overrides), featureList: [Feature.BranchSupport] } | |||
); | |||
} |
@@ -39,7 +39,6 @@ export interface BranchLikeRowProps { | |||
displayPurgeSetting?: boolean; | |||
onDelete: () => void; | |||
onRename: () => void; | |||
onUpdatePurgeSetting: () => void; | |||
} | |||
function BranchLikeRow(props: BranchLikeRowProps) { | |||
@@ -58,16 +57,12 @@ function BranchLikeRow(props: BranchLikeRowProps) { | |||
</span> | |||
</td> | |||
<td className="nowrap"> | |||
<BranchStatus branchLike={branchLike} component={component} /> | |||
<BranchStatus branchLike={branchLike} /> | |||
</td> | |||
<td className="nowrap">{<DateFromNow date={branchLike.analysisDate} />}</td> | |||
{displayPurgeSetting && isBranch(branchLike) && ( | |||
<td className="nowrap js-test-purge-toggle-container"> | |||
<BranchPurgeSetting | |||
branch={branchLike} | |||
component={component} | |||
onUpdatePurgeSetting={props.onUpdatePurgeSetting} | |||
/> | |||
<BranchPurgeSetting branch={branchLike} component={component} /> | |||
</td> | |||
)} | |||
<td className="nowrap"> |
@@ -31,7 +31,6 @@ export interface BranchLikeTableProps { | |||
displayPurgeSetting?: boolean; | |||
onDelete: (branchLike: BranchLike) => void; | |||
onRename: (branchLike: BranchLike) => void; | |||
onUpdatePurgeSetting: () => void; | |||
title: string; | |||
} | |||
@@ -81,7 +80,6 @@ function BranchLikeTable(props: BranchLikeTableProps) { | |||
key={getBranchLikeKey(branchLike)} | |||
onDelete={() => props.onDelete(branchLike)} | |||
onRename={() => props.onRename(branchLike)} | |||
onUpdatePurgeSetting={props.onUpdatePurgeSetting} | |||
/> | |||
))} | |||
</tbody> |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { useState } from 'react'; | |||
import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs'; | |||
import BranchIcon from '../../../components/icons/BranchIcon'; | |||
import PullRequestIcon from '../../../components/icons/PullRequestIcon'; | |||
@@ -29,22 +30,15 @@ import { | |||
sortPullRequests, | |||
} from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
import { Branch, BranchLike, PullRequest } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
import BranchLikeTable from './BranchLikeTable'; | |||
import DeleteBranchModal from './DeleteBranchModal'; | |||
import RenameBranchModal from './RenameBranchModal'; | |||
interface Props { | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
onBranchesChange: () => void; | |||
} | |||
interface State { | |||
currentTab: Tabs; | |||
deleting?: BranchLike; | |||
renaming?: BranchLike; | |||
} | |||
export enum Tabs { | |||
@@ -77,87 +71,57 @@ const TABS = [ | |||
}, | |||
]; | |||
export default class BranchLikeTabs extends React.PureComponent<Props, State> { | |||
state: State = { currentTab: Tabs.Branch }; | |||
handleTabSelect = (currentTab: Tabs) => { | |||
this.setState({ currentTab }); | |||
}; | |||
handleDeleteBranchLike = (branchLike: BranchLike) => { | |||
this.setState({ deleting: branchLike }); | |||
}; | |||
handleRenameBranchLike = (branchLike: BranchLike) => { | |||
this.setState({ renaming: branchLike }); | |||
}; | |||
handleUpdatePurgeSetting = () => { | |||
this.props.onBranchesChange(); | |||
}; | |||
export default function BranchLikeTabs(props: Props) { | |||
const { component } = props; | |||
const [currentTab, setCurrentTab] = useState<Tabs>(Tabs.Branch); | |||
const [renaming, setRenaming] = useState<BranchLike>(); | |||
handleClose = () => { | |||
this.setState({ deleting: undefined, renaming: undefined }); | |||
}; | |||
const [deleting, setDeleting] = useState<BranchLike>(); | |||
handleModalActionFulfilled = () => { | |||
this.handleClose(); | |||
this.props.onBranchesChange(); | |||
const handleClose = () => { | |||
setRenaming(undefined); | |||
setDeleting(undefined); | |||
}; | |||
render() { | |||
const { branchLikes, component } = this.props; | |||
const { currentTab, deleting, renaming } = this.state; | |||
const isBranchMode = currentTab === Tabs.Branch; | |||
const branchLikesToDisplay: BranchLike[] = isBranchMode | |||
? sortBranches(branchLikes.filter(isBranch)) | |||
: sortPullRequests(branchLikes.filter(isPullRequest)); | |||
const title = translate( | |||
isBranchMode | |||
? 'project_branch_pull_request.table.branch' | |||
: 'project_branch_pull_request.table.pull_request' | |||
); | |||
return ( | |||
<> | |||
<BoxedTabs | |||
className="branch-like-tabs" | |||
onSelect={this.handleTabSelect} | |||
selected={currentTab} | |||
tabs={TABS} | |||
const { data: { branchLikes } = { branchLikes: [] } } = useBranchesQuery(component); | |||
const isBranchMode = currentTab === Tabs.Branch; | |||
const branchLikesToDisplay: BranchLike[] = isBranchMode | |||
? sortBranches(branchLikes.filter(isBranch) as Branch[]) | |||
: sortPullRequests(branchLikes.filter(isPullRequest) as PullRequest[]); | |||
const title = translate( | |||
isBranchMode | |||
? 'project_branch_pull_request.table.branch' | |||
: 'project_branch_pull_request.table.pull_request' | |||
); | |||
return ( | |||
<> | |||
<BoxedTabs | |||
className="branch-like-tabs" | |||
onSelect={setCurrentTab} | |||
selected={currentTab} | |||
tabs={TABS} | |||
/> | |||
<div role="tabpanel" id={getTabPanelId(currentTab)} aria-labelledby={getTabId(currentTab)}> | |||
<BranchLikeTable | |||
branchLikes={branchLikesToDisplay} | |||
component={component} | |||
displayPurgeSetting={isBranchMode} | |||
onDelete={setDeleting} | |||
onRename={setRenaming} | |||
title={title} | |||
/> | |||
</div> | |||
<div role="tabpanel" id={getTabPanelId(currentTab)} aria-labelledby={getTabId(currentTab)}> | |||
<BranchLikeTable | |||
branchLikes={branchLikesToDisplay} | |||
component={component} | |||
displayPurgeSetting={isBranchMode} | |||
onDelete={this.handleDeleteBranchLike} | |||
onRename={this.handleRenameBranchLike} | |||
onUpdatePurgeSetting={this.handleUpdatePurgeSetting} | |||
title={title} | |||
/> | |||
</div> | |||
{deleting && ( | |||
<DeleteBranchModal branchLike={deleting} component={component} onClose={handleClose} /> | |||
)} | |||
{deleting && ( | |||
<DeleteBranchModal | |||
branchLike={deleting} | |||
component={component} | |||
onClose={this.handleClose} | |||
onDelete={this.handleModalActionFulfilled} | |||
/> | |||
)} | |||
{renaming && isMainBranch(renaming) && ( | |||
<RenameBranchModal | |||
branch={renaming} | |||
component={component} | |||
onClose={this.handleClose} | |||
onRename={this.handleModalActionFulfilled} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
{renaming && isMainBranch(renaming) && ( | |||
<RenameBranchModal branch={renaming} component={component} onClose={handleClose} /> | |||
)} | |||
</> | |||
); | |||
} |
@@ -18,88 +18,44 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { excludeBranchFromPurge } from '../../../api/branches'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import Toggle from '../../../components/controls/Toggle'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import { isMainBranch } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useExcludeFromPurgeMutation } from '../../../queries/branch'; | |||
import { Branch } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
onUpdatePurgeSetting: () => void; | |||
} | |||
interface State { | |||
excludedFromPurge: boolean; | |||
loading: boolean; | |||
} | |||
export default class BranchPurgeSetting extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { excludedFromPurge: props.branch.excludedFromPurge, loading: false }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
export default function BranchPurgeSetting(props: Props) { | |||
const { branch, component } = props; | |||
const { mutate: excludeFromPurge, isLoading } = useExcludeFromPurgeMutation(); | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleOnChange = () => { | |||
const { branch, component } = this.props; | |||
const { excludedFromPurge } = this.state; | |||
const newValue = !excludedFromPurge; | |||
this.setState({ loading: true }); | |||
excludeBranchFromPurge(component.key, branch.name, newValue) | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
excludedFromPurge: newValue, | |||
loading: false, | |||
}); | |||
this.props.onUpdatePurgeSetting(); | |||
} | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
const handleOnChange = (exclude: boolean) => { | |||
excludeFromPurge({ component, key: branch.name, exclude }); | |||
}; | |||
render() { | |||
const { branch } = this.props; | |||
const { excludedFromPurge, loading } = this.state; | |||
const isTheMainBranch = isMainBranch(branch); | |||
const disabled = isTheMainBranch || loading; | |||
return ( | |||
<> | |||
<Toggle disabled={disabled} onChange={this.handleOnChange} value={excludedFromPurge} /> | |||
<span className="spacer-left"> | |||
<DeferredSpinner loading={loading} /> | |||
</span> | |||
{isTheMainBranch && ( | |||
<HelpTooltip | |||
overlay={translate( | |||
'project_branch_pull_request.branch.auto_deletion.main_branch_tooltip' | |||
)} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
const isTheMainBranch = isMainBranch(branch); | |||
const disabled = isTheMainBranch || isLoading; | |||
return ( | |||
<> | |||
<Toggle disabled={disabled} onChange={handleOnChange} value={branch.excludedFromPurge} /> | |||
<span className="spacer-left"> | |||
<DeferredSpinner loading={isLoading} /> | |||
</span> | |||
{isTheMainBranch && ( | |||
<HelpTooltip | |||
overlay={translate( | |||
'project_branch_pull_request.branch.auto_deletion.main_branch_tooltip' | |||
)} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -18,11 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { deleteBranch, deletePullRequest } from '../../../api/branches'; | |||
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; | |||
import { getBranchLikeDisplayName, isPullRequest } from '../../../helpers/branch-like'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { useDeletBranchMutation } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
@@ -30,83 +30,50 @@ interface Props { | |||
branchLike: BranchLike; | |||
component: Component; | |||
onClose: () => void; | |||
onDelete: () => void; | |||
} | |||
interface State { | |||
loading: boolean; | |||
} | |||
export default class DeleteBranchModal extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: false }; | |||
export default function DeleteBranchModal(props: Props) { | |||
const { branchLike, component } = props; | |||
const { mutate: deleteBranch, isLoading } = useDeletBranchMutation(); | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
this.setState({ loading: true }); | |||
const request = isPullRequest(this.props.branchLike) | |||
? deletePullRequest({ | |||
project: this.props.component.key, | |||
pullRequest: this.props.branchLike.key, | |||
}) | |||
: deleteBranch({ | |||
branch: this.props.branchLike.name, | |||
project: this.props.component.key, | |||
}); | |||
request.then( | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
this.props.onDelete(); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
deleteBranch( | |||
{ component, branchLike }, | |||
{ | |||
onSuccess: props.onClose, | |||
} | |||
); | |||
}; | |||
render() { | |||
const { branchLike } = this.props; | |||
const header = translate( | |||
isPullRequest(branchLike) | |||
? 'project_branch_pull_request.pull_request.delete' | |||
: 'project_branch_pull_request.branch.delete' | |||
); | |||
const header = translate( | |||
isPullRequest(branchLike) | |||
? 'project_branch_pull_request.pull_request.delete' | |||
: 'project_branch_pull_request.branch.delete' | |||
); | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
{translateWithParameters( | |||
isPullRequest(branchLike) | |||
? 'project_branch_pull_request.pull_request.delete.are_you_sure' | |||
: 'project_branch_pull_request.branch.delete.are_you_sure', | |||
getBranchLikeDisplayName(branchLike) | |||
)} | |||
</div> | |||
<footer className="modal-foot"> | |||
{this.state.loading && <i className="spinner spacer-right" />} | |||
<SubmitButton className="button-red" disabled={this.state.loading}> | |||
{translate('delete')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<form onSubmit={handleSubmit}> | |||
<div className="modal-body"> | |||
{translateWithParameters( | |||
isPullRequest(branchLike) | |||
? 'project_branch_pull_request.pull_request.delete.are_you_sure' | |||
: 'project_branch_pull_request.branch.delete.are_you_sure', | |||
getBranchLikeDisplayName(branchLike) | |||
)} | |||
</div> | |||
<footer className="modal-foot"> | |||
{isLoading && <i className="spinner spacer-right" />} | |||
<SubmitButton className="button-red" disabled={isLoading}> | |||
{translate('delete')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} |
@@ -18,12 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { renameBranch } from '../../../api/branches'; | |||
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; | |||
import { useState } from 'react'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; | |||
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; | |||
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { useRenameMainBranchMutation } from '../../../queries/branch'; | |||
import { MainBranch } from '../../../types/branch-like'; | |||
import { Component } from '../../../types/types'; | |||
@@ -31,90 +32,62 @@ interface Props { | |||
branch: MainBranch; | |||
component: Component; | |||
onClose: () => void; | |||
onRename: () => void; | |||
} | |||
interface State { | |||
loading: boolean; | |||
name?: string; | |||
} | |||
export default class RenameBranchModal extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
export default function RenameBranchModal(props: Props) { | |||
const { branch, component } = props; | |||
const [name, setName] = useState<string>(); | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
const { mutate: renameMainBranch, isLoading } = useRenameMainBranchMutation(); | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
if (!this.state.name) { | |||
if (!name) { | |||
return; | |||
} | |||
this.setState({ loading: true }); | |||
renameBranch(this.props.component.key, this.state.name).then( | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
this.props.onRename(); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
renameMainBranch({ component, name }, { onSuccess: props.onClose }); | |||
}; | |||
handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
this.setState({ name: event.currentTarget.value }); | |||
const handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
setName(event.currentTarget.value); | |||
}; | |||
render() { | |||
const { branch } = this.props; | |||
const header = translate('project_branch_pull_request.branch.rename'); | |||
const submitDisabled = | |||
this.state.loading || !this.state.name || this.state.name === branch.name; | |||
const header = translate('project_branch_pull_request.branch.rename'); | |||
const submitDisabled = isLoading || !name || name === branch.name; | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small"> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-body"> | |||
<MandatoryFieldsExplanation className="modal-field" /> | |||
<div className="modal-field"> | |||
<label htmlFor="rename-branch-name"> | |||
{translate('new_name')} | |||
<MandatoryFieldMarker /> | |||
</label> | |||
<input | |||
autoFocus | |||
id="rename-branch-name" | |||
maxLength={100} | |||
name="name" | |||
onChange={this.handleNameChange} | |||
required | |||
size={50} | |||
type="text" | |||
value={this.state.name !== undefined ? this.state.name : branch.name} | |||
/> | |||
</div> | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={props.onClose} size="small"> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<form onSubmit={handleSubmit}> | |||
<div className="modal-body"> | |||
<MandatoryFieldsExplanation className="modal-field" /> | |||
<div className="modal-field"> | |||
<label htmlFor="rename-branch-name"> | |||
{translate('new_name')} | |||
<MandatoryFieldMarker /> | |||
</label> | |||
<input | |||
autoFocus | |||
id="rename-branch-name" | |||
maxLength={100} | |||
name="name" | |||
onChange={handleNameChange} | |||
required | |||
size={50} | |||
type="text" | |||
value={name ?? branch.name} | |||
/> | |||
</div> | |||
<footer className="modal-foot"> | |||
{this.state.loading && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitDisabled}>{translate('rename')}</SubmitButton> | |||
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} | |||
</div> | |||
<footer className="modal-foot"> | |||
{isLoading && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitDisabled}>{translate('rename')}</SubmitButton> | |||
<ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} |
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { FCProps } from '../../../../helpers/testUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import CoverageFilter from '../CoverageFilter'; | |||
it('renders options', () => { | |||
@@ -47,7 +47,7 @@ it('updates the filter query', async () => { | |||
expect(onQueryChange).toHaveBeenCalledWith({ coverage: '3' }); | |||
}); | |||
function renderCoverageFilter(props: Partial<FCProps<typeof CoverageFilter>> = {}) { | |||
function renderCoverageFilter(props: Partial<ComponentPropsType<typeof CoverageFilter>> = {}) { | |||
renderComponent( | |||
<CoverageFilter | |||
maxFacetValue={9} |
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { FCProps } from '../../../../helpers/testUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { LanguagesFilter } from '../LanguagesFilter'; | |||
it('renders language names', () => { | |||
@@ -61,7 +61,7 @@ it('updates the filter query', async () => { | |||
expect(onQueryChange).toHaveBeenCalledWith({ languages: 'java' }); | |||
}); | |||
function renderLanguagesFilter(props: Partial<FCProps<typeof LanguagesFilter>> = {}) { | |||
function renderLanguagesFilter(props: Partial<ComponentPropsType<typeof LanguagesFilter>> = {}) { | |||
renderComponent( | |||
<LanguagesFilter | |||
languages={{ |
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { FCProps } from '../../../../helpers/testUtils'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import QualityGateFacet from '../QualityGateFilter'; | |||
it('renders options', () => { | |||
@@ -57,7 +57,7 @@ it('handles multiselection', async () => { | |||
expect(onQueryChange).toHaveBeenCalledWith({ gate: 'OK,ERROR' }); | |||
}); | |||
function renderQualityGateFilter(props: Partial<FCProps<typeof QualityGateFacet>> = {}) { | |||
function renderQualityGateFilter(props: Partial<ComponentPropsType<typeof QualityGateFacet>> = {}) { | |||
renderComponent( | |||
<QualityGateFacet | |||
maxFacetValue={9} |
@@ -21,7 +21,6 @@ import { flatMap, range } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getMeasures } from '../../api/measures'; | |||
import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots'; | |||
import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; | |||
import withComponentContext from '../../app/components/componentContext/withComponentContext'; | |||
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; | |||
import { Location, Router, withRouter } from '../../components/hoc/withRouter'; | |||
@@ -30,6 +29,7 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe | |||
import { isInput } from '../../helpers/keyboardEventHelpers'; | |||
import { KeyboardKeys } from '../../helpers/keycodes'; | |||
import { getStandards } from '../../helpers/security-standard'; | |||
import { withBranchLikes } from '../../queries/branch'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { SecurityStandard, Standards } from '../../types/security'; | |||
import { | |||
@@ -46,11 +46,8 @@ import './styles.css'; | |||
import { SECURITY_STANDARDS, getLocations } from './utils'; | |||
const PAGE_SIZE = 500; | |||
interface DispatchProps { | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; | |||
} | |||
interface OwnProps { | |||
interface Props { | |||
branchLike?: BranchLike; | |||
currentUser: CurrentUser; | |||
component: Component; | |||
@@ -58,8 +55,6 @@ interface OwnProps { | |||
router: Router; | |||
} | |||
type Props = DispatchProps & OwnProps; | |||
interface State { | |||
filterByCategory?: { standard: SecurityStandard; category: string }; | |||
filterByCWE?: string; | |||
@@ -117,6 +112,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
componentDidUpdate(previous: Props) { | |||
if ( | |||
!isSameBranchLike(this.props.branchLike, previous.branchLike) || | |||
this.props.component.key !== previous.component.key || | |||
this.props.location.query.hotspots !== previous.location.query.hotspots || | |||
SECURITY_STANDARDS.some((s) => this.props.location.query[s] !== previous.location.query[s]) || | |||
@@ -434,13 +430,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
handleHotspotUpdate = (hotspotKey: string) => { | |||
const { hotspots, hotspotsPageIndex } = this.state; | |||
const { branchLike, component } = this.props; | |||
const index = hotspots.findIndex((h) => h.key === hotspotKey); | |||
if (isPullRequest(branchLike)) { | |||
this.props.fetchBranchStatus(branchLike, component.key); | |||
} | |||
return Promise.all( | |||
range(hotspotsPageIndex).map((p) => | |||
this.fetchSecurityHotspots(p + 1 /* pages are 1-indexed */) | |||
@@ -550,5 +541,5 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
} | |||
export default withRouter( | |||
withComponentContext(withCurrentUserContext(withBranchStatusActions(SecurityHotspotsApp))) | |||
withComponentContext(withCurrentUserContext(withBranchLikes(SecurityHotspotsApp))) | |||
); |
@@ -21,11 +21,11 @@ import { act, screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import React from 'react'; | |||
import { Route } from 'react-router-dom'; | |||
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; | |||
import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock'; | |||
import SecurityHotspotServiceMock from '../../../api/mocks/SecurityHotspotServiceMock'; | |||
import { getSecurityHotspots, setSecurityHotspotStatus } from '../../../api/security-hotspots'; | |||
import { searchUsers } from '../../../api/users'; | |||
import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint'; | |||
import { get, save } from '../../../helpers/storage'; | |||
@@ -107,6 +107,7 @@ const ui = { | |||
const originalScrollTo = window.scrollTo; | |||
const hotspotsHandler = new SecurityHotspotServiceMock(); | |||
const rulesHandles = new CodingRulesServiceMock(); | |||
const branchHandler = new BranchesServiceMock(); | |||
let showDialog = 'true'; | |||
jest.mocked(save).mockImplementation((_key: string, value?: string) => { | |||
@@ -143,6 +144,7 @@ beforeEach(() => { | |||
afterEach(() => { | |||
hotspotsHandler.reset(); | |||
rulesHandles.reset(); | |||
branchHandler.reset(); | |||
}); | |||
describe('rendering', () => { | |||
@@ -309,6 +311,7 @@ describe('navigation', () => { | |||
const user = userEvent.setup(); | |||
renderSecurityHotspotsApp(); | |||
expect(await ui.hotspotTitle(/'3' is a magic number./).find()).toBeInTheDocument(); | |||
await user.keyboard('{ArrowDown}'); | |||
expect(await ui.hotspotTitle(/'2' is a magic number./).find()).toBeInTheDocument(); | |||
await user.keyboard('{ArrowUp}'); | |||
@@ -343,16 +346,13 @@ describe('navigation', () => { | |||
const rtl = renderSecurityHotspotsApp( | |||
'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=test-1' | |||
); | |||
expect(await ui.hotspotTitle(/'3' is a magic number./).find()).toBeInTheDocument(); | |||
// On specific branch | |||
rtl.unmount(); | |||
renderSecurityHotspotsApp( | |||
'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=b1-test-1&branch=b1', | |||
{ branchLike: mockBranch({ name: 'b1' }) } | |||
'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=b1-test-1&branch=normal-branch' | |||
); | |||
expect(await ui.hotspotTitle(/'F' is a magic number./).find()).toBeInTheDocument(); | |||
}); | |||
@@ -417,7 +417,7 @@ it('should be able to filter the hotspot list', async () => { | |||
await user.click(ui.filterDropdown.get()); | |||
await user.click(ui.filterAssigneeToMe.get()); | |||
expect(ui.noHotspotForFilter.get()).toBeInTheDocument(); | |||
expect(await ui.noHotspotForFilter.find()).toBeInTheDocument(); | |||
await user.click(ui.filterToReview.get()); | |||
@@ -432,7 +432,7 @@ it('should be able to filter the hotspot list', async () => { | |||
}); | |||
await user.click(ui.filterDropdown.get()); | |||
await user.click(ui.filterNewCode.get()); | |||
await user.click(await ui.filterNewCode.find()); | |||
expect(getSecurityHotspots).toHaveBeenLastCalledWith({ | |||
inNewCodePeriod: true, | |||
@@ -458,15 +458,15 @@ function renderSecurityHotspotsApp( | |||
'security_hotspots', | |||
() => <Route path="security_hotspots" element={<SecurityHotspotsApp />} />, | |||
{ | |||
navigateTo, | |||
navigateTo: | |||
navigateTo ?? | |||
'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed', | |||
currentUser: mockLoggedInUser({ | |||
login: 'foo', | |||
name: 'foo', | |||
}), | |||
}, | |||
{ | |||
branchLike: mockMainBranch(), | |||
onBranchesChange: jest.fn(), | |||
onComponentChange: jest.fn(), | |||
component: mockComponent({ | |||
key: 'guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed', |
@@ -41,6 +41,7 @@ import { | |||
getPathUrlAsString, | |||
getRuleUrl, | |||
} from '../../../helpers/urls'; | |||
import { useRefreshBranchStatus } from '../../../queries/branch'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { SecurityStandard, Standards } from '../../../types/security'; | |||
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots'; | |||
@@ -68,6 +69,7 @@ interface StyledHeaderProps { | |||
export function HotspotHeader(props: HotspotHeaderProps) { | |||
const { hotspot, component, branchLike, standards, tabs, isCompressed, isScrolled } = props; | |||
const { message, messageFormattings, rule, key } = hotspot; | |||
const refrechBranchStatus = useRefreshBranchStatus(); | |||
const permalink = getPathUrlAsString( | |||
getComponentSecurityHotspotsUrl(component.key, { | |||
@@ -78,14 +80,15 @@ export function HotspotHeader(props: HotspotHeaderProps) { | |||
); | |||
const categoryStandard = standards?.[SecurityStandard.SONARSOURCE][rule.securityCategory]?.title; | |||
const handleStatusChange = async (statusOption: HotspotStatusOption) => { | |||
await props.onUpdateHotspot(true, statusOption); | |||
refrechBranchStatus(); | |||
}; | |||
const content = isCompressed ? ( | |||
<div className="sw-flex sw-justify-between"> | |||
{tabs} | |||
<StatusReviewButton | |||
hotspot={hotspot} | |||
onStatusChange={(statusOption) => props.onUpdateHotspot(true, statusOption)} | |||
/> | |||
<StatusReviewButton hotspot={hotspot} onStatusChange={handleStatusChange} /> | |||
</div> | |||
) : ( | |||
<> | |||
@@ -110,10 +113,7 @@ export function HotspotHeader(props: HotspotHeaderProps) { | |||
{rule.key} | |||
</Link> | |||
</div> | |||
<Status | |||
hotspot={hotspot} | |||
onStatusChange={(statusOption) => props.onUpdateHotspot(true, statusOption)} | |||
/> | |||
<Status hotspot={hotspot} onStatusChange={handleStatusChange} /> | |||
</div> | |||
<div className="sw-flex sw-flex-col sw-gap-4"> | |||
<HotspotHeaderRightSection |
@@ -77,7 +77,6 @@ export interface Props { | |||
highlightedLocationMessage?: { index: number; text: string | undefined }; | |||
onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; | |||
onLocationSelect?: (index: number) => void; | |||
onIssueChange?: (issue: Issue) => void; | |||
onIssueSelect?: (issueKey: string) => void; | |||
onIssueUnselect?: () => void; | |||
selectedIssue?: string; | |||
@@ -466,9 +465,6 @@ export default class SourceViewer extends React.PureComponent<Props, State> { | |||
); | |||
return { issues: newIssues, issuesByLine: issuesByLine(newIssues) }; | |||
}); | |||
if (this.props.onIssueChange) { | |||
this.props.onIssueChange(issue); | |||
} | |||
}; | |||
renderDuplicationPopup = (index: number, line: number) => { |
@@ -370,7 +370,6 @@ function renderSourceViewer(override?: Partial<SourceViewer['props']>) { | |||
component={componentsHandler.getNonEmptyFileKey()} | |||
displayAllIssues | |||
displayLocationMarkers | |||
onIssueChange={jest.fn()} | |||
onIssueSelect={jest.fn()} | |||
onLoaded={jest.fn()} | |||
onLocationSelect={jest.fn()} | |||
@@ -385,7 +384,6 @@ function renderSourceViewer(override?: Partial<SourceViewer['props']>) { | |||
component={componentsHandler.getNonEmptyFileKey()} | |||
displayAllIssues | |||
displayLocationMarkers | |||
onIssueChange={jest.fn()} | |||
onIssueSelect={jest.fn()} | |||
onLoaded={jest.fn()} | |||
onLocationSelect={jest.fn()} |
@@ -21,6 +21,7 @@ import { Note } from 'design-system'; | |||
import * as React from 'react'; | |||
import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { useBranchesQuery } from '../../queries/branch'; | |||
import { AnalysisEvent } from '../../types/project-activity'; | |||
import Tooltip from '../controls/Tooltip'; | |||
import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner'; | |||
@@ -32,16 +33,12 @@ export interface EventInnerProps { | |||
} | |||
export default function EventInner({ event, readonly }: EventInnerProps) { | |||
const { component } = React.useContext(ComponentContext); | |||
const { data: { branchLike } = {} } = useBranchesQuery(component); | |||
if (isRichQualityGateEvent(event)) { | |||
return <RichQualityGateEventInner event={event} readonly={readonly} />; | |||
} else if (isDefinitionChangeEvent(event)) { | |||
return ( | |||
<ComponentContext.Consumer> | |||
{({ branchLike }) => ( | |||
<DefinitionChangeEventInner branchLike={branchLike} event={event} readonly={readonly} /> | |||
)} | |||
</ComponentContext.Consumer> | |||
); | |||
return <DefinitionChangeEventInner branchLike={branchLike} event={event} readonly={readonly} />; | |||
} | |||
return ( | |||
<Tooltip overlay={event.description}> |
@@ -26,7 +26,7 @@ import { mockHistoryItem, mockMeasureHistory } from '../../../helpers/mocks/proj | |||
import { mockMetric } from '../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector'; | |||
import { FCProps } from '../../../helpers/testUtils'; | |||
import { ComponentPropsType } from '../../../helpers/testUtils'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { GraphType, MeasureHistory } from '../../../types/project-activity'; | |||
import { Metric } from '../../../types/types'; | |||
@@ -238,7 +238,7 @@ function getPageObject() { | |||
function renderActivityGraph( | |||
graphsHistoryProps: Partial<GraphsHistory['props']> = {}, | |||
graphsHeaderProps: Partial<FCProps<typeof GraphsHeader>> = {} | |||
graphsHeaderProps: Partial<ComponentPropsType<typeof GraphsHeader>> = {} | |||
) { | |||
function ActivityGraph() { | |||
const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]); |
@@ -21,13 +21,13 @@ import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { Route } from 'react-router-dom'; | |||
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; | |||
import { isMainBranch } from '../../../helpers/branch-like'; | |||
import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like'; | |||
import { mockAnalysisEvent } from '../../../helpers/mocks/project-activity'; | |||
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils'; | |||
import { byRole, byText } from '../../../helpers/testSelector'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentContextShape } from '../../../types/component'; | |||
import { Branch, BranchLike } from '../../../types/branch-like'; | |||
import { | |||
ApplicationAnalysisEventCategory, | |||
DefinitionChangeType, | |||
@@ -43,8 +43,8 @@ const ui = { | |||
definitionChangeLabel: byText('event.category.DEFINITION_CHANGE', { exact: false }), | |||
projectAddedTxt: (branch: BranchLike) => | |||
isMainBranch(branch) | |||
? byText('event.definition_change.added') | |||
: byText('event.definition_change.branch_added'), | |||
? byText(/event\.definition_change\.added/) | |||
: byText(/event\.definition_change\.branch_added/), | |||
projectRemovedTxt: (branch: BranchLike) => | |||
isMainBranch(branch) | |||
? byText('event.definition_change.removed') | |||
@@ -57,10 +57,17 @@ const ui = { | |||
versionLabel: byText('event.category.VERSION', { exact: false }), | |||
}; | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => { | |||
handler.reset(); | |||
}); | |||
describe('DEFINITION_CHANGE events', () => { | |||
it.each([mockMainBranch(), mockBranch()])( | |||
'should render correctly for "ADDED" events', | |||
async (branchLike: BranchLike) => { | |||
async (branchLike: Branch) => { | |||
handler.addBranch(branchLike); | |||
const user = userEvent.setup(); | |||
renderEventInner( | |||
{ | |||
@@ -78,14 +85,14 @@ describe('DEFINITION_CHANGE events', () => { | |||
}, | |||
}), | |||
}, | |||
{ branchLike } | |||
`branch=${branchLike.name}&id=my-project` | |||
); | |||
expect(ui.definitionChangeLabel.get()).toBeInTheDocument(); | |||
expect(await ui.definitionChangeLabel.find()).toBeInTheDocument(); | |||
await user.click(ui.showMoreBtn.get()); | |||
expect(ui.projectAddedTxt(branchLike).get()).toBeInTheDocument(); | |||
expect(await ui.projectAddedTxt(branchLike).find()).toBeInTheDocument(); | |||
expect(ui.projectLink('Foo').get()).toBeInTheDocument(); | |||
expect(screen.getByText('master-foo')).toBeInTheDocument(); | |||
} | |||
@@ -93,8 +100,9 @@ describe('DEFINITION_CHANGE events', () => { | |||
it.each([mockMainBranch(), mockBranch()])( | |||
'should render correctly for "REMOVED" events', | |||
async (branchLike: BranchLike) => { | |||
async (branchLike: Branch) => { | |||
const user = userEvent.setup(); | |||
handler.addBranch(branchLike); | |||
renderEventInner( | |||
{ | |||
event: mockAnalysisEvent({ | |||
@@ -111,14 +119,14 @@ describe('DEFINITION_CHANGE events', () => { | |||
}, | |||
}), | |||
}, | |||
{ branchLike } | |||
`branch=${branchLike.name}&id=my-project` | |||
); | |||
expect(ui.definitionChangeLabel.get()).toBeInTheDocument(); | |||
await user.click(ui.showMoreBtn.get()); | |||
expect(ui.projectRemovedTxt(branchLike).get()).toBeInTheDocument(); | |||
expect(await ui.projectRemovedTxt(branchLike).find()).toBeInTheDocument(); | |||
expect(ui.projectLink('Bar').get()).toBeInTheDocument(); | |||
expect(screen.getByText('master-bar')).toBeInTheDocument(); | |||
} | |||
@@ -228,14 +236,10 @@ describe('VERSION events', () => { | |||
}); | |||
}); | |||
function renderEventInner( | |||
props: Partial<EventInnerProps> = {}, | |||
componentContext: Partial<ComponentContextShape> = {} | |||
) { | |||
function renderEventInner(props: Partial<EventInnerProps> = {}, params?: string) { | |||
return renderAppWithComponentContext( | |||
'/', | |||
() => <Route path="*" element={<EventInner event={mockAnalysisEvent()} {...props} />} />, | |||
{}, | |||
componentContext | |||
{ navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project' } | |||
); | |||
} |
@@ -18,20 +18,19 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import withBranchStatus from '../../app/components/branch-status/withBranchStatus'; | |||
import Level from '../../components/ui/Level'; | |||
import { BranchStatusData } from '../../types/branch-like'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
export type BranchStatusProps = Pick<BranchStatusData, 'status'>; | |||
export interface BranchStatusProps { | |||
branchLike: BranchLike; | |||
} | |||
export function BranchStatus(props: BranchStatusProps) { | |||
const { status } = props; | |||
export default function BranchStatus(props: BranchStatusProps) { | |||
const { branchLike } = props; | |||
if (!status) { | |||
if (!branchLike.status) { | |||
return null; | |||
} | |||
return <Level level={status} small />; | |||
return <Level level={branchLike.status.qualityGateStatus} small />; | |||
} | |||
export default withBranchStatus(BranchStatus); |
@@ -18,13 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen, waitFor } from '@testing-library/react'; | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { getTask } from '../../../api/ce'; | |||
import { AnalysisWarningsModal } from '../../../app/components/nav/component/AnalysisWarningsModal'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockTaskWarning } from '../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser } from '../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { AnalysisWarningsModal } from '../AnalysisWarningsModal'; | |||
import { ComponentPropsType } from '../../../helpers/testUtils'; | |||
jest.mock('../../../api/ce', () => ({ | |||
dismissAnalysisWarning: jest.fn().mockResolvedValue(null), | |||
@@ -60,26 +61,12 @@ describe('should render correctly', () => { | |||
}); | |||
}); | |||
it('should not fetch task warnings if it does not have to', () => { | |||
renderAnalysisWarningsModal(); | |||
expect(getTask).not.toHaveBeenCalled(); | |||
}); | |||
it('should fetch task warnings if it has to', async () => { | |||
renderAnalysisWarningsModal({ taskId: 'abcd1234', warnings: undefined }); | |||
expect(screen.queryByText('message foo')).not.toBeInTheDocument(); | |||
expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']); | |||
await waitFor(() => { | |||
expect(screen.getByText('message foo')).toBeInTheDocument(); | |||
}); | |||
}); | |||
function renderAnalysisWarningsModal(props: Partial<AnalysisWarningsModal['props']> = {}) { | |||
function renderAnalysisWarningsModal( | |||
props: Partial<ComponentPropsType<typeof AnalysisWarningsModal>> = {} | |||
) { | |||
return renderComponent( | |||
<AnalysisWarningsModal | |||
component={mockComponent()} | |||
currentUser={mockCurrentUser({ isLoggedIn: true })} | |||
onClose={jest.fn()} | |||
warnings={[ |
@@ -17,24 +17,34 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { BranchStatus, BranchStatusProps } from '../BranchStatus'; | |||
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; | |||
import { mockBranch } from '../../../helpers/mocks/branch-like'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import BranchStatus, { BranchStatusProps } from '../BranchStatus'; | |||
it('should render correctly', () => { | |||
expect(shallowRender().type()).toBeNull(); | |||
expect( | |||
shallowRender({ | |||
status: 'OK', | |||
}) | |||
).toMatchSnapshot('Successful'); | |||
expect( | |||
shallowRender({ | |||
status: 'ERROR', | |||
}) | |||
).toMatchSnapshot('Error'); | |||
const handler = new BranchesServiceMock(); | |||
beforeEach(() => { | |||
handler.reset(); | |||
}); | |||
it('should render ok status', async () => { | |||
renderBranchStatus({ branchLike: mockBranch({ status: { qualityGateStatus: 'OK' } }) }); | |||
expect(await screen.findByText('OK')).toBeInTheDocument(); | |||
}); | |||
it('should render error status', async () => { | |||
renderBranchStatus({ branchLike: mockBranch({ status: { qualityGateStatus: 'ERROR' } }) }); | |||
expect(await screen.findByText('ERROR')).toBeInTheDocument(); | |||
}); | |||
function shallowRender(overrides: Partial<BranchStatusProps> = {}) { | |||
return shallow(<BranchStatus {...overrides} />); | |||
function renderBranchStatus(overrides: Partial<BranchStatusProps> = {}) { | |||
const defaultProps = { | |||
branchLike: mockBranch(), | |||
} as const; | |||
return renderComponent(<BranchStatus {...defaultProps} {...overrides} />); | |||
} |
@@ -1,15 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: Error 1`] = ` | |||
<Level | |||
level="ERROR" | |||
small={true} | |||
/> | |||
`; | |||
exports[`should render correctly: Successful 1`] = ` | |||
<Level | |||
level="OK" | |||
small={true} | |||
/> | |||
`; |
@@ -21,7 +21,7 @@ import styled from '@emotion/styled'; | |||
import * as React from 'react'; | |||
import { colors, sizes } from '../../app/theme'; | |||
export interface BoxedTabsProps<K extends string | number> { | |||
export interface BoxedTabsProps<K> { | |||
className?: string; | |||
onSelect: (key: K) => void; | |||
selected?: K; | |||
@@ -72,7 +72,7 @@ const ActiveBorder = styled.div<{ active: boolean }>` | |||
top: -1px; | |||
`; | |||
export default function BoxedTabs<K extends string | number>(props: BoxedTabsProps<K>) { | |||
export default function BoxedTabs<K>(props: BoxedTabsProps<K>) { | |||
const { className, tabs, selected } = props; | |||
return ( | |||
@@ -96,10 +96,10 @@ export default function BoxedTabs<K extends string | number>(props: BoxedTabsPro | |||
); | |||
} | |||
export function getTabPanelId(key: string | number) { | |||
export function getTabPanelId<K>(key: K) { | |||
return `tabpanel-${key}`; | |||
} | |||
export function getTabId(key: string | number) { | |||
export function getTabId<K>(key: K) { | |||
return `tab-${key}`; | |||
} |
@@ -17,11 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { flow } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useCallback } from 'react'; | |||
import { setIssueAssignee } from '../../api/issues'; | |||
import { isInput, isShortcut } from '../../helpers/keyboardEventHelpers'; | |||
import { KeyboardKeys } from '../../helpers/keycodes'; | |||
import { getKeyboardShortcutEnabled } from '../../helpers/preferences'; | |||
import { useRefreshBranchStatus } from '../../queries/branch'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { Issue as TypeIssue } from '../../types/types'; | |||
import { updateIssue } from './actions'; | |||
@@ -41,89 +44,93 @@ interface Props { | |||
selected: boolean; | |||
} | |||
export default class Issue extends React.PureComponent<Props> { | |||
static defaultProps = { | |||
selected: false, | |||
}; | |||
export default function Issue(props: Props) { | |||
const { | |||
selected = false, | |||
issue, | |||
branchLike, | |||
checked, | |||
openPopup, | |||
displayWhyIsThisAnIssue, | |||
onCheck, | |||
onPopupToggle, | |||
} = props; | |||
componentDidMount() { | |||
if (this.props.selected) { | |||
document.addEventListener('keydown', this.handleKeyDown, { capture: true }); | |||
} | |||
} | |||
const refreshStatus = useRefreshBranchStatus(); | |||
componentDidUpdate(prevProps: Props) { | |||
if (!prevProps.selected && this.props.selected) { | |||
document.addEventListener('keydown', this.handleKeyDown, { capture: true }); | |||
} else if (prevProps.selected && !this.props.selected) { | |||
document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); | |||
} | |||
} | |||
const onChange = flow([props.onChange, refreshStatus]); | |||
componentWillUnmount() { | |||
if (this.props.selected) { | |||
document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); | |||
} | |||
} | |||
const togglePopup = useCallback( | |||
(popupName: string, open?: boolean) => { | |||
onPopupToggle(issue.key, popupName, open); | |||
}, | |||
[issue.key, onPopupToggle] | |||
); | |||
handleKeyDown = (event: KeyboardEvent) => { | |||
if (!getKeyboardShortcutEnabled() || isInput(event) || isShortcut(event)) { | |||
return true; | |||
} else if (event.key === KeyboardKeys.KeyF) { | |||
event.preventDefault(); | |||
return this.togglePopup('transition'); | |||
} else if (event.key === KeyboardKeys.KeyA) { | |||
event.preventDefault(); | |||
return this.togglePopup('assign'); | |||
} else if (event.key === KeyboardKeys.KeyM && this.props.issue.actions.includes('assign')) { | |||
event.preventDefault(); | |||
return this.handleAssignement('_me'); | |||
} else if (event.key === KeyboardKeys.KeyI) { | |||
event.preventDefault(); | |||
return this.togglePopup('set-severity'); | |||
} else if (event.key === KeyboardKeys.KeyC) { | |||
event.preventDefault(); | |||
return this.togglePopup('comment'); | |||
} else if (event.key === KeyboardKeys.KeyT) { | |||
event.preventDefault(); | |||
return this.togglePopup('edit-tags'); | |||
} else if (event.key === KeyboardKeys.Space) { | |||
event.preventDefault(); | |||
if (this.props.onCheck) { | |||
return this.props.onCheck(this.props.issue.key); | |||
const handleAssignement = useCallback( | |||
(login: string) => { | |||
if (issue.assignee !== login) { | |||
updateIssue(onChange, setIssueAssignee({ issue: issue.key, assignee: login })); | |||
} | |||
} | |||
return true; | |||
}; | |||
togglePopup('assign', false); | |||
}, | |||
[issue.assignee, issue.key, onChange, togglePopup] | |||
); | |||
togglePopup = (popupName: string, open?: boolean) => { | |||
this.props.onPopupToggle(this.props.issue.key, popupName, open); | |||
}; | |||
const handleKeyDown = useCallback( | |||
(event: KeyboardEvent) => { | |||
if (!getKeyboardShortcutEnabled() || isInput(event) || isShortcut(event)) { | |||
return true; | |||
} else if (event.key === KeyboardKeys.KeyF) { | |||
event.preventDefault(); | |||
return togglePopup('transition'); | |||
} else if (event.key === KeyboardKeys.KeyA) { | |||
event.preventDefault(); | |||
return togglePopup('assign'); | |||
} else if (event.key === KeyboardKeys.KeyM && issue.actions.includes('assign')) { | |||
event.preventDefault(); | |||
return handleAssignement('_me'); | |||
} else if (event.key === KeyboardKeys.KeyI) { | |||
event.preventDefault(); | |||
return togglePopup('set-severity'); | |||
} else if (event.key === KeyboardKeys.KeyC) { | |||
event.preventDefault(); | |||
return togglePopup('comment'); | |||
} else if (event.key === KeyboardKeys.KeyT) { | |||
event.preventDefault(); | |||
return togglePopup('edit-tags'); | |||
} else if (event.key === KeyboardKeys.Space) { | |||
event.preventDefault(); | |||
if (onCheck) { | |||
return onCheck(issue.key); | |||
} | |||
} | |||
return true; | |||
}, | |||
[issue.actions, issue.key, togglePopup, handleAssignement, onCheck] | |||
); | |||
handleAssignement = (login: string) => { | |||
const { issue } = this.props; | |||
if (issue.assignee !== login) { | |||
updateIssue(this.props.onChange, setIssueAssignee({ issue: issue.key, assignee: login })); | |||
React.useEffect(() => { | |||
if (selected) { | |||
document.addEventListener('keydown', handleKeyDown, { capture: true }); | |||
} | |||
this.togglePopup('assign', false); | |||
}; | |||
return () => document.removeEventListener('keydown', handleKeyDown, { capture: true }); | |||
}, [handleKeyDown, selected]); | |||
render() { | |||
return ( | |||
<IssueView | |||
branchLike={this.props.branchLike} | |||
checked={this.props.checked} | |||
currentPopup={this.props.openPopup} | |||
displayWhyIsThisAnIssue={this.props.displayWhyIsThisAnIssue} | |||
issue={this.props.issue} | |||
onAssign={this.handleAssignement} | |||
onChange={this.props.onChange} | |||
onCheck={this.props.onCheck} | |||
onClick={this.props.onClick} | |||
onSelect={this.props.onSelect} | |||
selected={this.props.selected} | |||
togglePopup={this.togglePopup} | |||
/> | |||
); | |||
} | |||
return ( | |||
<IssueView | |||
branchLike={branchLike} | |||
checked={checked} | |||
currentPopup={openPopup} | |||
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue} | |||
issue={issue} | |||
onAssign={handleAssignement} | |||
onChange={onChange} | |||
onCheck={props.onCheck} | |||
onClick={props.onClick} | |||
onSelect={props.onSelect} | |||
selected={selected} | |||
togglePopup={togglePopup} | |||
/> | |||
); | |||
} |
@@ -28,6 +28,7 @@ import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; | |||
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole, byText } from '../../../helpers/testSelector'; | |||
import { ComponentPropsType } from '../../../helpers/testUtils'; | |||
import { | |||
IssueActions, | |||
IssueSeverity, | |||
@@ -416,8 +417,12 @@ function getPageObject() { | |||
return { ui, user }; | |||
} | |||
function renderIssue(props: Partial<Omit<Issue['props'], 'onChange' | 'onPopupToggle'>> = {}) { | |||
function Wrapper(wrapperProps: Omit<Issue['props'], 'onChange' | 'onPopupToggle'>) { | |||
function renderIssue( | |||
props: Partial<Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'>> = {} | |||
) { | |||
function Wrapper( | |||
wrapperProps: Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'> | |||
) { | |||
const [issue, setIssue] = React.useState(wrapperProps.issue); | |||
const [openPopup, setOpenPopup] = React.useState<string | undefined>(); | |||
return ( |
@@ -21,12 +21,9 @@ import * as React from 'react'; | |||
import { getAlmSettingsNoCatch } from '../../api/alm-settings'; | |||
import { getScannableProjects } from '../../api/components'; | |||
import { getValue } from '../../api/settings'; | |||
import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; | |||
import { isMainBranch } from '../../helpers/branch-like'; | |||
import { getHostUrl } from '../../helpers/urls'; | |||
import { hasGlobalPermission } from '../../helpers/users'; | |||
import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings'; | |||
import { MainBranch } from '../../types/branch-like'; | |||
import { Permissions } from '../../types/permissions'; | |||
import { SettingsKey } from '../../types/settings'; | |||
import { Component } from '../../types/types'; | |||
@@ -50,8 +47,6 @@ interface State { | |||
loading: boolean; | |||
} | |||
const DEFAULT_MAIN_BRANCH_NAME = 'main'; | |||
export class TutorialSelection extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
@@ -121,25 +116,17 @@ export class TutorialSelection extends React.PureComponent<Props, State> { | |||
const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial; | |||
return ( | |||
<ComponentContext.Consumer> | |||
{({ branchLikes }) => ( | |||
<TutorialSelectionRenderer | |||
almBinding={almBinding} | |||
baseUrl={baseUrl} | |||
component={component} | |||
currentUser={currentUser} | |||
currentUserCanScanProject={currentUserCanScanProject} | |||
loading={loading} | |||
mainBranchName={ | |||
(branchLikes.find((b) => isMainBranch(b)) as MainBranch | undefined)?.name || | |||
DEFAULT_MAIN_BRANCH_NAME | |||
} | |||
projectBinding={projectBinding} | |||
selectedTutorial={selectedTutorial} | |||
willRefreshAutomatically={willRefreshAutomatically} | |||
/> | |||
)} | |||
</ComponentContext.Consumer> | |||
<TutorialSelectionRenderer | |||
almBinding={almBinding} | |||
baseUrl={baseUrl} | |||
component={component} | |||
currentUser={currentUser} | |||
currentUserCanScanProject={currentUserCanScanProject} | |||
loading={loading} | |||
projectBinding={projectBinding} | |||
selectedTutorial={selectedTutorial} | |||
willRefreshAutomatically={willRefreshAutomatically} | |||
/> | |||
); | |||
} | |||
} |
@@ -28,10 +28,13 @@ import { | |||
Title, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { isMainBranch } from '../../helpers/branch-like'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getBaseUrl } from '../../helpers/system'; | |||
import { getProjectTutorialLocation, getProjectUrl } from '../../helpers/urls'; | |||
import { useBranchesQuery } from '../../queries/branch'; | |||
import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings'; | |||
import { MainBranch } from '../../types/branch-like'; | |||
import { Component } from '../../types/types'; | |||
import { LoggedInUser } from '../../types/users'; | |||
import { Alert } from '../ui/Alert'; | |||
@@ -43,6 +46,8 @@ import JenkinsTutorial from './jenkins/JenkinsTutorial'; | |||
import OtherTutorial from './other/OtherTutorial'; | |||
import { TutorialModes } from './types'; | |||
const DEFAULT_MAIN_BRANCH_NAME = 'main'; | |||
export interface TutorialSelectionRendererProps { | |||
almBinding?: AlmSettingsInstance; | |||
baseUrl: string; | |||
@@ -50,7 +55,6 @@ export interface TutorialSelectionRendererProps { | |||
currentUser: LoggedInUser; | |||
currentUserCanScanProject: boolean; | |||
loading: boolean; | |||
mainBranchName: string; | |||
projectBinding?: ProjectAlmBindingResponse; | |||
selectedTutorial?: TutorialModes; | |||
willRefreshAutomatically?: boolean; | |||
@@ -85,11 +89,17 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender | |||
currentUser, | |||
currentUserCanScanProject, | |||
loading, | |||
mainBranchName, | |||
projectBinding, | |||
selectedTutorial, | |||
willRefreshAutomatically, | |||
} = props; | |||
const { data: { branchLikes } = { branchLikes: [] } } = useBranchesQuery(component); | |||
const mainBranchName = | |||
(branchLikes.find((b) => isMainBranch(b)) as MainBranch | undefined)?.name || | |||
DEFAULT_MAIN_BRANCH_NAME; | |||
if (loading) { | |||
return <i aria-label={translate('loading')} className="spinner" />; | |||
} |
@@ -25,24 +25,23 @@ import { getAlmSettingsNoCatch } from '../../../api/alm-settings'; | |||
import { getScannableProjects } from '../../../api/components'; | |||
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; | |||
import UserTokensMock from '../../../api/mocks/UserTokensMock'; | |||
import { | |||
mockGithubBindingDefinition, | |||
mockProjectAlmBindingResponse, | |||
} from '../../../helpers/mocks/alm-settings'; | |||
import { mockProjectAlmBindingResponse } from '../../../helpers/mocks/alm-settings'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockLoggedInUser } from '../../../helpers/testMocks'; | |||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole, byText } from '../../../helpers/testSelector'; | |||
import { ComponentPropsType } from '../../../helpers/testUtils'; | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
import { Feature } from '../../../types/features'; | |||
import { Permissions } from '../../../types/permissions'; | |||
import { SettingsKey } from '../../../types/settings'; | |||
import { withRouter } from '../../hoc/withRouter'; | |||
import { TutorialSelection } from '../TutorialSelection'; | |||
import TutorialSelection from '../TutorialSelection'; | |||
import { TutorialModes } from '../types'; | |||
jest.mock('../../../api/user-tokens'); | |||
jest.mock('../../../api/branches'); | |||
jest.mock('../../../helpers/urls', () => ({ | |||
...jest.requireActual('../../../helpers/urls'), | |||
getHostUrl: jest.fn().mockReturnValue('http://host.url'), | |||
@@ -120,9 +119,11 @@ it.each([ | |||
}); | |||
it('should correctly fetch the corresponding ALM setting', async () => { | |||
(getAlmSettingsNoCatch as jest.Mock).mockResolvedValueOnce([ | |||
mockGithubBindingDefinition({ key: 'binding', url: 'https://enterprise.github.com' }), | |||
]); | |||
jest | |||
.mocked(getAlmSettingsNoCatch) | |||
.mockResolvedValueOnce([ | |||
{ key: 'binding', url: 'https://enterprise.github.com', alm: AlmKeys.GitHub }, | |||
]); | |||
const user = userEvent.setup(); | |||
renderTutorialSelection( | |||
{ | |||
@@ -160,7 +161,9 @@ it('should fallback on the host URL', async () => { | |||
}); | |||
it('should not display a warning if the user has no global scan permission, but can scan the project', async () => { | |||
(getScannableProjects as jest.Mock).mockResolvedValueOnce({ projects: [{ key: 'foo' }] }); | |||
jest | |||
.mocked(getScannableProjects) | |||
.mockResolvedValueOnce({ projects: [{ key: 'foo', name: 'foo' }] }); | |||
renderTutorialSelection({ currentUser: mockLoggedInUser() }); | |||
await waitOnDataLoaded(); | |||
@@ -194,16 +197,12 @@ async function startJenkinsTutorial(user: UserEvent) { | |||
} | |||
function renderTutorialSelection( | |||
props: Partial<TutorialSelection['props']> = {}, | |||
props: Partial<ComponentPropsType<typeof TutorialSelection>> = {}, | |||
navigateTo: string = 'dashboard?id=bar' | |||
) { | |||
const Wrapper = withRouter(({ location, ...subProps }: TutorialSelection['props']) => { | |||
return <TutorialSelection location={location} {...subProps} />; | |||
}); | |||
return renderApp( | |||
'/dashboard', | |||
<Wrapper | |||
<TutorialSelection | |||
component={mockComponent({ key: 'foo' })} | |||
currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Scan] } })} | |||
{...props} |
@@ -17,13 +17,8 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { debounce } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getParents } from '../../api/components'; | |||
import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; | |||
import { isPullRequest } from '../../helpers/branch-like'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { Issue, SourceViewerFile } from '../../types/types'; | |||
import { SourceViewerFile } from '../../types/types'; | |||
import SourceViewer from '../SourceViewer/SourceViewer'; | |||
import WorkspaceComponentTitle from './WorkspaceComponentTitle'; | |||
import WorkspaceHeader, { Props as WorkspaceHeaderProps } from './WorkspaceHeader'; | |||
@@ -31,20 +26,14 @@ import { ComponentDescriptor } from './context'; | |||
export interface Props extends Omit<WorkspaceHeaderProps, 'children' | 'onClose'> { | |||
component: ComponentDescriptor; | |||
fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; | |||
height: number; | |||
onClose: (componentKey: string) => void; | |||
onLoad: (details: { key: string; name: string; qualifier: string }) => void; | |||
} | |||
export class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
export default class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
container?: HTMLElement | null; | |||
constructor(props: Props) { | |||
super(props); | |||
this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000); | |||
} | |||
componentDidMount() { | |||
if (document.documentElement) { | |||
document.documentElement.classList.add('with-workspace'); | |||
@@ -61,10 +50,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
this.props.onClose(this.props.component.key); | |||
}; | |||
handleIssueChange = (_: Issue) => { | |||
this.refreshBranchStatus(); | |||
}; | |||
handleLoaded = (component: SourceViewerFile) => { | |||
this.props.onLoad({ | |||
key: this.props.component.key, | |||
@@ -82,21 +67,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
} | |||
}; | |||
refreshBranchStatus = () => { | |||
const { component } = this.props; | |||
const { branchLike } = component; | |||
if (branchLike && isPullRequest(branchLike)) { | |||
getParents(component.key).then( | |||
(parents?: any[]) => { | |||
if (parents && parents.length > 0) { | |||
this.props.fetchBranchStatus(branchLike, parents.pop().key); | |||
} | |||
}, | |||
() => {} | |||
); | |||
} | |||
}; | |||
render() { | |||
const { component } = this.props; | |||
@@ -123,7 +93,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
branchLike={component.branchLike} | |||
component={component.key} | |||
highlightedLine={component.line} | |||
onIssueChange={this.handleIssueChange} | |||
onLoaded={this.handleLoaded} | |||
/> | |||
</div> | |||
@@ -131,5 +100,3 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> { | |||
); | |||
} | |||
} | |||
export default withBranchStatusActions(WorkspaceComponentViewer); |
@@ -19,11 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getParents } from '../../../api/components'; | |||
import { mockPullRequest } from '../../../helpers/mocks/branch-like'; | |||
import { mockIssue } from '../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { Props, WorkspaceComponentViewer } from '../WorkspaceComponentViewer'; | |||
import WorkspaceComponentViewer, { Props } from '../WorkspaceComponentViewer'; | |||
jest.mock('../../../api/components', () => ({ | |||
getParents: jest.fn().mockResolvedValue([{ key: 'bar' }]), | |||
@@ -55,28 +51,10 @@ it('should call back after load', () => { | |||
expect(onLoad).toHaveBeenCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' }); | |||
}); | |||
it('should refresh branch status if issues are updated', async () => { | |||
const fetchBranchStatus = jest.fn(); | |||
const branchLike = mockPullRequest(); | |||
const component = { | |||
branchLike, | |||
key: 'foo', | |||
}; | |||
const wrapper = shallowRender({ component, fetchBranchStatus }); | |||
const instance = wrapper.instance(); | |||
await waitAndUpdate(wrapper); | |||
instance.handleIssueChange(mockIssue()); | |||
expect(getParents).toHaveBeenCalledWith(component.key); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, 'bar'); | |||
}); | |||
function shallowRender(props?: Partial<Props>) { | |||
return shallow<WorkspaceComponentViewer>( | |||
<WorkspaceComponentViewer | |||
component={{ branchLike: undefined, key: 'foo' }} | |||
fetchBranchStatus={jest.fn()} | |||
height={300} | |||
onClose={jest.fn()} | |||
onCollapse={jest.fn()} |
@@ -53,7 +53,7 @@ exports[`should render correctly: open component 1`] = ` | |||
} | |||
} | |||
/> | |||
<withBranchStatusActions(WorkspaceComponentViewer) | |||
<WorkspaceComponentViewer | |||
component={ | |||
{ | |||
"branchLike": { |
@@ -34,7 +34,6 @@ exports[`should render 1`] = ` | |||
displayIssueLocationsCount={true} | |||
displayIssueLocationsLink={true} | |||
displayLocationMarkers={true} | |||
onIssueChange={[Function]} | |||
onLoaded={[Function]} | |||
/> | |||
</div> |
@@ -23,11 +23,9 @@ import { | |||
BranchLike, | |||
BranchLikeTree, | |||
BranchParameters, | |||
BranchStatusData, | |||
MainBranch, | |||
PullRequest, | |||
} from '../types/branch-like'; | |||
import { Dict } from '../types/types'; | |||
export function isBranch(branchLike?: BranchLike): branchLike is Branch { | |||
return branchLike !== undefined && (branchLike as Branch).isMain !== undefined; | |||
@@ -139,12 +137,3 @@ export function fillBranchLike( | |||
} | |||
return undefined; | |||
} | |||
export function getBranchStatusByBranchLike( | |||
branchStatusByComponent: Dict<Dict<BranchStatusData>>, | |||
component: string, | |||
branchLike: BranchLike | |||
): BranchStatusData { | |||
const branchLikeKey = getBranchLikeKey(branchLike); | |||
return branchStatusByComponent[component] && branchStatusByComponent[component][branchLikeKey]; | |||
} |
@@ -20,6 +20,7 @@ | |||
import { | |||
QualityGateApplicationStatus, | |||
QualityGateProjectStatus, | |||
QualityGateProjectStatusCondition, | |||
QualityGateStatus, | |||
QualityGateStatusCondition, | |||
QualityGateStatusConditionEnhanced, | |||
@@ -48,6 +49,20 @@ export function mockQualityGateStatus( | |||
}; | |||
} | |||
export function mockQualityGateProjectCondition( | |||
overrides: Partial<QualityGateProjectStatusCondition> = {} | |||
): QualityGateProjectStatusCondition { | |||
return { | |||
actualValue: '10', | |||
errorThreshold: '0', | |||
status: 'ERROR', | |||
metricKey: 'foo', | |||
comparator: 'GT', | |||
periodIndex: 1, | |||
...overrides, | |||
}; | |||
} | |||
export function mockQualityGateStatusCondition( | |||
overrides: Partial<QualityGateStatusCondition> = {} | |||
): QualityGateStatusCondition { |
@@ -94,7 +94,7 @@ export function renderAppWithAdminContext( | |||
export function renderComponent( | |||
component: React.ReactElement, | |||
pathname = '/', | |||
{ appState = mockAppState() }: RenderContext = {} | |||
{ appState = mockAppState(), featureList = [] }: RenderContext = {} | |||
) { | |||
function Wrapper({ children }: { children: React.ReactElement }) { | |||
const queryClient = new QueryClient(); | |||
@@ -103,13 +103,15 @@ export function renderComponent( | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<QueryClientProvider client={queryClient}> | |||
<HelmetProvider> | |||
<AppStateContextProvider appState={appState}> | |||
<MemoryRouter initialEntries={[pathname]}> | |||
<Routes> | |||
<Route path="*" element={children} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</AppStateContextProvider> | |||
<AvailableFeaturesContext.Provider value={featureList}> | |||
<AppStateContextProvider appState={appState}> | |||
<MemoryRouter initialEntries={[pathname]}> | |||
<Routes> | |||
<Route path="*" element={children} /> | |||
</Routes> | |||
</MemoryRouter> | |||
</AppStateContextProvider> | |||
</AvailableFeaturesContext.Provider> | |||
</HelmetProvider> | |||
</QueryClientProvider> | |||
</IntlProvider> | |||
@@ -132,8 +134,6 @@ export function renderAppWithComponentContext( | |||
return ( | |||
<ComponentContext.Provider | |||
value={{ | |||
branchLikes: [], | |||
onBranchesChange: jest.fn(), | |||
onComponentChange: (changes: Partial<Component>) => { | |||
setRealComponent({ ...realComponent, ...changes }); | |||
}, |
@@ -52,6 +52,15 @@ export interface ReactTestingQuery { | |||
byLabelText(...args: Parameters<BoundFunction<GetByText>>): ReactTestingQuery; | |||
byTestId(...args: Parameters<BoundFunction<GetByBoundAttribute>>): ReactTestingQuery; | |||
byDisplayValue(...args: Parameters<BoundFunction<GetByBoundAttribute>>): ReactTestingQuery; | |||
getAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T; | |||
findAt<T extends HTMLElement = HTMLElement>( | |||
index: number, | |||
container?: HTMLElement, | |||
waitForOptions?: waitForOptions | |||
): Promise<T>; | |||
queryAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T | null; | |||
} | |||
abstract class ChainingQuery implements ReactTestingQuery { | |||
@@ -73,6 +82,26 @@ abstract class ChainingQuery implements ReactTestingQuery { | |||
abstract queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[] | null; | |||
getAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T { | |||
return this.getAll<T>(container)[index]; | |||
} | |||
async findAt<T extends HTMLElement = HTMLElement>( | |||
index: number, | |||
container?: HTMLElement, | |||
waitForOptions?: waitForOptions | |||
): Promise<T> { | |||
return (await this.findAll<T>(container, waitForOptions))[index]; | |||
} | |||
queryAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T | null { | |||
const all = this.queryAll<T>(container); | |||
if (all) { | |||
return all[index]; | |||
} | |||
return null; | |||
} | |||
byText(...args: Parameters<BoundFunction<GetByText>>): ReactTestingQuery { | |||
return new ChainDispatch(this, new DispatchByText(args)); | |||
} |