diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-05-31 16:22:08 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-06-10 20:03:26 +0000 |
commit | 2902cdedeafb9803bc21464a4b50a05f2a7e0ded (patch) | |
tree | bdce466b066365ce8a5ca720fe7b6f13ca5a9148 /server | |
parent | cac8b706867b5b56813df253516b2431d7f47889 (diff) | |
download | sonarqube-2902cdedeafb9803bc21464a4b50a05f2a7e0ded.tar.gz sonarqube-2902cdedeafb9803bc21464a4b50a05f2a7e0ded.zip |
SONAR-14872 Display warning if PR deco cannot happen
Diffstat (limited to 'server')
13 files changed, 757 insertions, 179 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index caf68e4eb0e..d1cf0ae7c18 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -20,11 +20,13 @@ import { differenceBy } from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; -import { getProjectAlmBinding } from '../../api/alm-settings'; +import { HttpStatus } from 'sonar-ui-common/helpers/request'; +import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings'; import { getBranches, getPullRequests } from '../../api/branches'; import { getAnalysisStatus, getTasksForComponent } from '../../api/ce'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/nav'; +import { withAppState } from '../../components/hoc/withAppState'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { getBranchLikeQuery, @@ -34,9 +36,12 @@ import { } from '../../helpers/branch-like'; import { getPortfolioUrl } from '../../helpers/urls'; import { registerBranchStatus, requireAuthorization } from '../../store/rootActions'; -import { ProjectAlmBindingResponse } from '../../types/alm-settings'; +import { + ProjectAlmBindingConfigurationErrors, + ProjectAlmBindingResponse +} from '../../types/alm-settings'; import { BranchLike } from '../../types/branch-like'; -import { isPortfolioLike } from '../../types/component'; +import { ComponentQualifier, isPortfolioLike } from '../../types/component'; import { Task, TaskStatuses, TaskWarning } from '../../types/tasks'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import { ComponentContext } from './ComponentContext'; @@ -44,6 +49,7 @@ import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToInd import ComponentNav from './nav/component/ComponentNav'; interface Props { + appState: Pick<T.AppState, 'branchesEnabled'>; children: React.ReactElement; location: Pick<Location, 'query' | 'pathname'>; registerBranchStatus: (branchLike: BranchLike, component: string, status: T.Status) => void; @@ -59,6 +65,7 @@ interface State { isPending: boolean; loading: boolean; projectBinding?: ProjectAlmBindingResponse; + projectBindingErrors?: ProjectAlmBindingConfigurationErrors; tasksInProgress?: Task[]; warnings: TaskWarning[]; } @@ -90,96 +97,85 @@ export class ComponentContainer extends React.PureComponent<Props, State> { window.clearTimeout(this.watchStatusTimer); } - addQualifier = (component: T.Component) => ({ - ...component, - qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier - }); - - fetchComponent() { + fetchComponent = async () => { const { branch, id: key, pullRequest } = this.props.location.query; this.setState({ loading: true }); - const onError = (response?: Response) => { + let componentWithQualifier; + try { + const [nav, { component }] = await Promise.all([ + getComponentNavigation({ component: key, branch, pullRequest }), + getComponentData({ component: key, branch, pullRequest }) + ]); + componentWithQualifier = this.addQualifier({ ...nav, ...component }); + } catch (e) { if (this.mounted) { - if (response && response.status === 403) { + if (e && e.status === HttpStatus.Forbidden) { this.props.requireAuthorization(this.props.router); } else { this.setState({ component: undefined, loading: false }); } } - }; - - Promise.all([ - getComponentNavigation({ component: key, branch, pullRequest }), - getComponentData({ component: key, branch, pullRequest }), - getProjectAlmBinding(key).catch(() => undefined) - ]) - .then(([nav, { component }, projectBinding]) => { - const componentWithQualifier = this.addQualifier({ ...nav, ...component }); - - /* - * There used to be a redirect from /dashboard to /portfolio which caused issues. - * Links should be fixed to not rely on this redirect, but: - * This is a fail-safe in case there are still some faulty links remaining. - */ - if ( - this.props.location.pathname.match('dashboard') && - isPortfolioLike(componentWithQualifier.qualifier) - ) { - this.props.router.replace(getPortfolioUrl(component.key)); - } + return; + } - if (this.mounted) { - this.setState({ projectBinding }); - } + /* + * There used to be a redirect from /dashboard to /portfolio which caused issues. + * Links should be fixed to not rely on this redirect, but: + * This is a fail-safe in case there are still some faulty links remaining. + */ + if ( + this.props.location.pathname.match('dashboard') && + isPortfolioLike(componentWithQualifier.qualifier) + ) { + this.props.router.replace(getPortfolioUrl(componentWithQualifier.key)); + } - return componentWithQualifier; - }, onError) - .then(this.fetchBranches) - .then( - ({ branchLike, branchLikes, component }) => { - if (this.mounted) { - this.setState({ - branchLike, - branchLikes, - component, - loading: false - }); - this.fetchStatus(component); - this.fetchWarnings(component, branchLike); - } - }, - () => {} - ); - } + const { branchLike, branchLikes } = await this.fetchBranches(componentWithQualifier); + + const projectBinding = await getProjectAlmBinding(key).catch(() => undefined); - fetchBranches = ( - component: T.Component - ): Promise<{ - branchLike?: BranchLike; - branchLikes: BranchLike[]; - component: T.Component; - }> => { - const breadcrumb = component.breadcrumbs.find(({ qualifier }) => { - return ['APP', 'TRK'].includes(qualifier); + if (this.mounted) { + this.setState({ + branchLike, + branchLikes, + component: componentWithQualifier, + projectBinding, + loading: false + }); + + this.fetchStatus(componentWithQualifier); + this.fetchWarnings(componentWithQualifier, branchLike); + this.fetchProjectBindingErrors(componentWithQualifier); + } + }; + + fetchBranches = async (componentWithQualifier: T.Component) => { + 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; - return Promise.all([ + const [branches, pullRequests] = await Promise.all([ getBranches(key), - breadcrumb.qualifier === 'APP' ? Promise.resolve([]) : getPullRequests(key) - ]).then(([branches, pullRequests]) => { - const branchLikes = [...branches, ...pullRequests]; - const branchLike = this.getCurrentBranchLike(branchLikes); + breadcrumb.qualifier === ComponentQualifier.Application + ? Promise.resolve([]) + : getPullRequests(key) + ]); - this.registerBranchStatuses(branchLikes, component); + branchLikes = [...branches, ...pullRequests]; + branchLike = this.getCurrentBranchLike(branchLikes); - return { branchLike, branchLikes, component }; - }); - } else { - return Promise.resolve({ branchLikes: [], component }); + this.registerBranchStatuses(branchLikes, componentWithQualifier); } + + return { branchLike, branchLikes }; }; fetchStatus = (component: T.Component) => { @@ -237,7 +233,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { }; fetchWarnings = (component: T.Component, branchLike?: BranchLike) => { - if (component.qualifier === 'TRK') { + if (component.qualifier === ComponentQualifier.Project) { getAnalysisStatus({ component: component.key, ...getBranchLikeQuery(branchLike) @@ -250,6 +246,22 @@ export class ComponentContainer extends React.PureComponent<Props, State> { } }; + fetchProjectBindingErrors = async (component: T.Component) => { + if (component.analysisDate === undefined && this.props.appState.branchesEnabled) { + const projectBindingErrors = await validateProjectAlmBinding(component.key).catch( + () => undefined + ); + if (this.mounted) { + this.setState({ projectBindingErrors }); + } + } + }; + + addQualifier = (component: T.Component) => ({ + ...component, + qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier + }); + getCurrentBranchLike = (branchLikes: BranchLike[]) => { const { query } = this.props.location; return query.pullRequest @@ -347,27 +359,32 @@ export class ComponentContainer extends React.PureComponent<Props, State> { currentTask, isPending, projectBinding, + projectBindingErrors, tasksInProgress } = this.state; const isInProgress = tasksInProgress && tasksInProgress.length > 0; return ( <div> - {component && !['FIL', 'UTS'].includes(component.qualifier) && ( - <ComponentNav - branchLikes={branchLikes} - component={component} - currentBranchLike={branchLike} - currentTask={currentTask} - currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)} - isInProgress={isInProgress} - isPending={isPending} - onComponentChange={this.handleComponentChange} - onWarningDismiss={this.handleWarningDismiss} - projectBinding={projectBinding} - warnings={this.state.warnings} - /> - )} + {component && + !([ComponentQualifier.File, ComponentQualifier.TestFile] as string[]).includes( + component.qualifier + ) && ( + <ComponentNav + branchLikes={branchLikes} + component={component} + currentBranchLike={branchLike} + currentTask={currentTask} + currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)} + isInProgress={isInProgress} + isPending={isPending} + onComponentChange={this.handleComponentChange} + onWarningDismiss={this.handleWarningDismiss} + projectBinding={projectBinding} + projectBindingErrors={projectBindingErrors} + warnings={this.state.warnings} + /> + )} {loading ? ( <div className="page page-limited"> <i className="spinner" /> @@ -393,4 +410,4 @@ export class ComponentContainer extends React.PureComponent<Props, State> { const mapDispatchToProps = { registerBranchStatus, requireAuthorization }; -export default withRouter(connect(null, mapDispatchToProps)(ComponentContainer)); +export default withAppState(withRouter(connect(null, mapDispatchToProps)(ComponentContainer))); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index 57024478548..84fc4a8bb52 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -20,14 +20,15 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { getProjectAlmBinding } from '../../../api/alm-settings'; +import { getProjectAlmBinding, validateProjectAlmBinding } from '../../../api/alm-settings'; import { getBranches, getPullRequests } from '../../../api/branches'; import { getAnalysisStatus, getTasksForComponent } from '../../../api/ce'; import { getComponentData } from '../../../api/components'; import { getComponentNavigation } from '../../../api/nav'; +import { mockProjectAlmBindingConfigurationErrors } from '../../../helpers/mocks/alm-settings'; import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; import { mockTask } from '../../../helpers/mocks/tasks'; -import { mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks'; +import { mockAppState, mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks'; import { AlmKeys } from '../../../types/alm-settings'; import { ComponentQualifier } from '../../../types/component'; import { TaskStatuses } from '../../../types/tasks'; @@ -68,7 +69,8 @@ jest.mock('../../../api/nav', () => ({ })); jest.mock('../../../api/alm-settings', () => ({ - getProjectAlmBinding: jest.fn().mockResolvedValue(undefined) + getProjectAlmBinding: jest.fn().mockResolvedValue(undefined), + validateProjectAlmBinding: jest.fn().mockResolvedValue(undefined) })); // mock this, because some of its children are using redux store @@ -312,9 +314,36 @@ it('should correctly reload last task warnings if anything got dismissed', async expect(getAnalysisStatus).toBeCalledTimes(1); }); +describe('should correctly validate the project binding depending on the context', () => { + const COMPONENT = mockComponent({ + breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }] + }); + const PROJECT_BINDING_ERRORS = mockProjectAlmBindingConfigurationErrors(); + + it.each([ + ["has an analysis; won't perform any check", { ...COMPONENT, analysisDate: '2020-01' }], + ['has a project binding; check is OK', COMPONENT, undefined, 1], + ['has a project binding; check is not OK', COMPONENT, PROJECT_BINDING_ERRORS, 1] + ])('%s', async (_, component, projectBindingErrors = undefined, n = 0) => { + (getComponentNavigation as jest.Mock).mockResolvedValueOnce({}); + (getComponentData as jest.Mock<any>).mockResolvedValueOnce({ component }); + + if (n > 0) { + (validateProjectAlmBinding as jest.Mock).mockResolvedValueOnce(projectBindingErrors); + } + + const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) }); + await waitAndUpdate(wrapper); + expect(wrapper.state().projectBindingErrors).toBe(projectBindingErrors); + + expect(validateProjectAlmBinding).toBeCalledTimes(n); + }); +}); + function shallowRender(props: Partial<ComponentContainer['props']> = {}) { return shallow<ComponentContainer>( <ComponentContainer + appState={mockAppState()} location={mockLocation({ query: { id: 'foo' } })} registerBranchStatus={jest.fn()} requireAuthorization={jest.fn()} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index b0c0ab3c14d..1e896e28f9c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -20,13 +20,17 @@ import * as classNames from 'classnames'; import * as React from 'react'; import ContextNavBar from 'sonar-ui-common/components/ui/ContextNavBar'; -import { ProjectAlmBindingResponse } from '../../../../types/alm-settings'; +import { + ProjectAlmBindingConfigurationErrors, + ProjectAlmBindingResponse +} from '../../../../types/alm-settings'; import { BranchLike } from '../../../../types/branch-like'; import { ComponentQualifier } from '../../../../types/component'; import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks'; import { rawSizes } from '../../../theme'; import RecentHistory from '../../RecentHistory'; import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; +import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif'; import Header from './Header'; import HeaderMeta from './HeaderMeta'; import Menu from './Menu'; @@ -44,9 +48,12 @@ export interface ComponentNavProps { onComponentChange: (changes: Partial<T.Component>) => void; onWarningDismiss: () => void; projectBinding?: ProjectAlmBindingResponse; + projectBindingErrors?: ProjectAlmBindingConfigurationErrors; warnings: TaskWarning[]; } +const ALERT_HEIGHT = 30; + export default function ComponentNav(props: ComponentNavProps) { const { branchLikes, @@ -57,6 +64,7 @@ export default function ComponentNav(props: ComponentNavProps) { isInProgress, isPending, projectBinding, + projectBindingErrors, warnings } = props; const { contextNavHeightRaw, globalNavHeightRaw } = rawSizes; @@ -78,9 +86,11 @@ export default function ComponentNav(props: ComponentNavProps) { } }, [component, component.key]); - let notifComponent; + let contextNavHeight = contextNavHeightRaw; + + let bgTaskNotifComponent; if (isInProgress || isPending || (currentTask && currentTask.status === TaskStatuses.Failed)) { - notifComponent = ( + bgTaskNotifComponent = ( <ComponentNavBgTaskNotif component={component} currentTask={currentTask} @@ -89,12 +99,31 @@ export default function ComponentNav(props: ComponentNavProps) { isPending={isPending} /> ); + contextNavHeight += ALERT_HEIGHT; } - const contextNavHeight = notifComponent ? contextNavHeightRaw + 30 : contextNavHeightRaw; + let prDecoNotifComponent; + if (projectBindingErrors !== undefined) { + prDecoNotifComponent = ( + <ComponentNavProjectBindingErrorNotif + alm={projectBinding?.alm} + component={component} + projectBindingErrors={projectBindingErrors} + /> + ); + contextNavHeight += ALERT_HEIGHT; + } return ( - <ContextNavBar height={contextNavHeight} id="context-navigation" notif={notifComponent}> + <ContextNavBar + height={contextNavHeight} + id="context-navigation" + notif={ + <> + {bgTaskNotifComponent} + {prDecoNotifComponent} + </> + }> <div className={classNames('display-flex-center display-flex-space-between little-padded-top', { 'padded-bottom': warnings.length === 0 diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx index 0a87a66ba4f..19424db709e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx @@ -111,7 +111,7 @@ export class ComponentNavBgTaskNotif extends React.PureComponent<Props> { } return ( - <Alert display="banner" variant="error"> + <Alert className="null-spacer-bottom" display="banner" variant="error"> {message} </Alert> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx new file mode 100644 index 00000000000..1156c729560 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { + ALM_INTEGRATION, + PULL_REQUEST_DECORATION_BINDING_CATEGORY +} from '../../../../apps/settings/components/AdditionalCategoryKeys'; +import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; +import { hasGlobalPermission } from '../../../../helpers/users'; +import { + AlmKeys, + ProjectAlmBindingConfigurationErrors, + ProjectAlmBindingConfigurationErrorScope +} from '../../../../types/alm-settings'; +import { Permissions } from '../../../../types/permissions'; + +export interface ComponentNavProjectBindingErrorNotifProps { + alm?: AlmKeys; + component: T.Component; + currentUser: T.CurrentUser; + projectBindingErrors: ProjectAlmBindingConfigurationErrors; +} + +export function ComponentNavProjectBindingErrorNotif( + props: ComponentNavProjectBindingErrorNotifProps +) { + const { alm, component, currentUser, projectBindingErrors } = props; + const isSysadmin = hasGlobalPermission(currentUser, Permissions.Admin); + + let action; + if (projectBindingErrors.scope === ProjectAlmBindingConfigurationErrorScope.Global) { + if (isSysadmin) { + action = ( + <Link + to={{ + pathname: '/admin/settings', + query: { + category: ALM_INTEGRATION, + alm + } + }}> + {translate('component_navigation.pr_deco.action.check_global_settings')} + </Link> + ); + } else { + action = translate('component_navigation.pr_deco.action.contact_sys_admin'); + } + } else if (projectBindingErrors.scope === ProjectAlmBindingConfigurationErrorScope.Project) { + if (component.configuration?.showSettings) { + action = ( + <Link + to={{ + pathname: '/project/settings', + query: { category: PULL_REQUEST_DECORATION_BINDING_CATEGORY, id: component.key } + }}> + {translate('component_navigation.pr_deco.action.check_project_settings')} + </Link> + ); + } else { + action = translate('component_navigation.pr_deco.action.contact_project_admin'); + } + } + + return ( + <Alert display="banner" variant="warning"> + <FormattedMessage + defaultMessage={translate('component_navigation.pr_deco.error_detected_X')} + id="component_navigation.pr_deco.error_detected_X" + values={{ action }} + /> + </Alert> + ); +} + +export default withCurrentUser(ComponentNavProjectBindingErrorNotif); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index 7053107e433..47d8fbc25dc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings'; import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks'; import { mockComponent } from '../../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../../types/component'; @@ -41,6 +42,11 @@ it('renders correctly', () => { expect(shallowRender({ currentTask: mockTask({ status: TaskStatuses.Failed }) })).toMatchSnapshot( 'has failed notification' ); + expect( + shallowRender({ + projectBindingErrors: mockProjectAlmBindingConfigurationErrors() + }) + ).toMatchSnapshot('has failed project binding'); }); it('correctly adds data to the history if there are breadcrumbs', () => { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavProjectBindingErrorNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavProjectBindingErrorNotif-test.tsx new file mode 100644 index 00000000000..29a2feb410f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavProjectBindingErrorNotif-test.tsx @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings'; +import { mockComponent, mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; +import { + AlmKeys, + ProjectAlmBindingConfigurationErrorScope +} from '../../../../../types/alm-settings'; +import { Permissions } from '../../../../../types/permissions'; +import { + ComponentNavProjectBindingErrorNotif, + ComponentNavProjectBindingErrorNotifProps +} from '../ComponentNavProjectBindingErrorNotif'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('global, no admin'); + expect( + shallowRender({ + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }) + ).toMatchSnapshot('global, admin'); + expect( + shallowRender({ + projectBindingErrors: mockProjectAlmBindingConfigurationErrors({ + scope: ProjectAlmBindingConfigurationErrorScope.Project + }) + }) + ).toMatchSnapshot('project, no admin'); + expect( + shallowRender({ + component: mockComponent({ configuration: { showSettings: true } }), + projectBindingErrors: mockProjectAlmBindingConfigurationErrors({ + scope: ProjectAlmBindingConfigurationErrorScope.Project + }) + }) + ).toMatchSnapshot('project, admin'); + expect( + shallowRender({ + projectBindingErrors: mockProjectAlmBindingConfigurationErrors({ + scope: ProjectAlmBindingConfigurationErrorScope.Unknown + }) + }) + ).toMatchSnapshot('unknown'); +}); + +function shallowRender(props: Partial<ComponentNavProjectBindingErrorNotifProps> = {}) { + return shallow<ComponentNavProjectBindingErrorNotifProps>( + <ComponentNavProjectBindingErrorNotif + alm={AlmKeys.GitHub} + component={mockComponent()} + currentUser={mockCurrentUser()} + projectBindingErrors={mockProjectAlmBindingConfigurationErrors()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index a5669ebe18d..ca190a5ec8c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -4,6 +4,7 @@ exports[`renders correctly: default 1`] = ` <ContextNavBar height={72} id="context-navigation" + notif={<React.Fragment />} > <div className="display-flex-center display-flex-space-between little-padded-top padded-bottom" @@ -151,7 +152,59 @@ exports[`renders correctly: has failed notification 1`] = ` height={102} id="context-navigation" notif={ - <withRouter(ComponentNavBgTaskNotif) + <React.Fragment> + <withRouter(ComponentNavBgTaskNotif) + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + currentTask={ + Object { + "analysisId": "x123", + "componentKey": "foo", + "componentName": "Foo", + "componentQualifier": "TRK", + "id": "AXR8jg_0mF2ZsYr8Wzs2", + "status": "FAILED", + "submittedAt": "2020-09-11T11:45:35+0200", + "type": "REPORT", + } + } + isInProgress={false} + isPending={false} + /> + </React.Fragment> + } +> + <div + className="display-flex-center display-flex-space-between little-padded-top padded-bottom" + > + <Connect(Component) + branchLikes={Array []} component={ Object { "breadcrumbs": Array [ @@ -180,21 +233,164 @@ exports[`renders correctly: has failed notification 1`] = ` "tags": Array [], } } - currentTask={ + /> + <Connect(HeaderMeta) + component={ Object { - "analysisId": "x123", - "componentKey": "foo", - "componentName": "Foo", - "componentQualifier": "TRK", - "id": "AXR8jg_0mF2ZsYr8Wzs2", - "status": "FAILED", - "submittedAt": "2020-09-11T11:45:35+0200", - "type": "REPORT", + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], } } - isInProgress={false} - isPending={false} + onWarningDismiss={[MockFunction]} + warnings={Array []} /> + </div> + <Connect(withAppState(Menu)) + branchLikes={Array []} + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + isInProgress={false} + isPending={false} + onToggleProjectInfo={[Function]} + /> + <InfoDrawer + displayed={false} + onClose={[Function]} + top={120} + > + <Connect(ProjectInformation) + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + onComponentChange={[MockFunction]} + /> + </InfoDrawer> +</ContextNavBar> +`; + +exports[`renders correctly: has failed project binding 1`] = ` +<ContextNavBar + height={102} + id="context-navigation" + notif={ + <React.Fragment> + <Connect(withCurrentUser(ComponentNavProjectBindingErrorNotif)) + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + projectBindingErrors={ + Object { + "errors": Array [ + Object { + "msg": "Foo bar is not correct", + }, + Object { + "msg": "Bar baz has no permissions here", + }, + ], + "scope": "GLOBAL", + } + } + /> + </React.Fragment> } > <div @@ -343,38 +539,40 @@ exports[`renders correctly: has in progress notification 1`] = ` height={102} id="context-navigation" notif={ - <withRouter(ComponentNavBgTaskNotif) - component={ - Object { - "breadcrumbs": Array [ - Object { - "key": "foo", - "name": "Foo", - "qualifier": "TRK", - }, - ], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + <React.Fragment> + <withRouter(ComponentNavBgTaskNotif) + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - isInProgress={true} - isPending={false} - /> + isInProgress={true} + isPending={false} + /> + </React.Fragment> } > <div @@ -523,38 +721,40 @@ exports[`renders correctly: has pending notification 1`] = ` height={102} id="context-navigation" notif={ - <withRouter(ComponentNavBgTaskNotif) - component={ - Object { - "breadcrumbs": Array [ - Object { - "key": "foo", - "name": "Foo", - "qualifier": "TRK", - }, - ], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ - Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + <React.Fragment> + <withRouter(ComponentNavBgTaskNotif) + component={ + Object { + "breadcrumbs": Array [ + Object { + "key": "foo", + "name": "Foo", + "qualifier": "TRK", + }, + ], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - isInProgress={false} - isPending={true} - /> + isInProgress={false} + isPending={true} + /> + </React.Fragment> } > <div @@ -702,6 +902,7 @@ exports[`renders correctly: has warnings 1`] = ` <ContextNavBar height={72} id="context-navigation" + notif={<React.Fragment />} > <div className="display-flex-center display-flex-space-between little-padded-top" diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap index b5664b36ac5..cfed20bb431 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap @@ -2,6 +2,7 @@ exports[`renders correctly: default 1`] = ` <Alert + className="null-spacer-bottom" display="banner" variant="error" > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap new file mode 100644 index 00000000000..cbf60122bc6 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: global, admin 1`] = ` +<Alert + display="banner" + variant="warning" +> + <FormattedMessage + defaultMessage="component_navigation.pr_deco.error_detected_X" + id="component_navigation.pr_deco.error_detected_X" + values={ + Object { + "action": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/admin/settings", + "query": Object { + "alm": "github", + "category": "almintegration", + }, + } + } + > + component_navigation.pr_deco.action.check_global_settings + </Link>, + } + } + /> +</Alert> +`; + +exports[`should render correctly: global, no admin 1`] = ` +<Alert + display="banner" + variant="warning" +> + <FormattedMessage + defaultMessage="component_navigation.pr_deco.error_detected_X" + id="component_navigation.pr_deco.error_detected_X" + values={ + Object { + "action": "component_navigation.pr_deco.action.contact_sys_admin", + } + } + /> +</Alert> +`; + +exports[`should render correctly: project, admin 1`] = ` +<Alert + display="banner" + variant="warning" +> + <FormattedMessage + defaultMessage="component_navigation.pr_deco.error_detected_X" + id="component_navigation.pr_deco.error_detected_X" + values={ + Object { + "action": <Link + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/project/settings", + "query": Object { + "category": "pull_request_decoration_binding", + "id": "my-project", + }, + } + } + > + component_navigation.pr_deco.action.check_project_settings + </Link>, + } + } + /> +</Alert> +`; + +exports[`should render correctly: project, no admin 1`] = ` +<Alert + display="banner" + variant="warning" +> + <FormattedMessage + defaultMessage="component_navigation.pr_deco.error_detected_X" + id="component_navigation.pr_deco.error_detected_X" + values={ + Object { + "action": "component_navigation.pr_deco.action.contact_project_admin", + } + } + /> +</Alert> +`; + +exports[`should render correctly: unknown 1`] = ` +<Alert + display="banner" + variant="warning" +> + <FormattedMessage + defaultMessage="component_navigation.pr_deco.error_detected_X" + id="component_navigation.pr_deco.error_detected_X" + values={ + Object { + "action": undefined, + } + } + /> +</Alert> +`; diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index ae61dfde85c..3e5adbeecdf 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -69,6 +69,10 @@ th.hide-overflow { margin-top: 0 !important; } +.null-spacer-bottom { + margin-bottom: 0 !important; +} + .spacer { margin: 8px !important; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx index 48f82838adf..f20850762b6 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx @@ -33,15 +33,11 @@ import { } from '../../../../../api/alm-settings'; import { mockAlmSettingsInstance, + mockProjectAlmBindingConfigurationErrors, mockProjectAlmBindingResponse } from '../../../../../helpers/mocks/alm-settings'; import { mockComponent, mockCurrentUser } from '../../../../../helpers/testMocks'; -import { - AlmKeys, - AlmSettingsInstance, - ProjectAlmBindingConfigurationErrors, - ProjectAlmBindingConfigurationErrorScope -} from '../../../../../types/alm-settings'; +import { AlmKeys, AlmSettingsInstance } from '../../../../../types/alm-settings'; import { PRDecorationBinding } from '../PRDecorationBinding'; import PRDecorationBindingRenderer from '../PRDecorationBindingRenderer'; @@ -373,10 +369,7 @@ it('should call the validation WS and store errors', async () => { mockProjectAlmBindingResponse({ key: 'key' }) ); - const errors: ProjectAlmBindingConfigurationErrors = { - scope: ProjectAlmBindingConfigurationErrorScope.Global, - errors: [{ msg: 'Test' }, { msg: 'tesT' }] - }; + const errors = mockProjectAlmBindingConfigurationErrors(); (validateProjectAlmBinding as jest.Mock).mockRejectedValueOnce(errors); const wrapper = shallowRender(); diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts index a5cb9c7868b..0abd3e4ef53 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts @@ -27,6 +27,8 @@ import { BitbucketCloudBindingDefinition, GithubBindingDefinition, GitlabBindingDefinition, + ProjectAlmBindingConfigurationErrors, + ProjectAlmBindingConfigurationErrorScope, ProjectAlmBindingResponse, ProjectAzureBindingResponse, ProjectBitbucketBindingResponse, @@ -198,3 +200,13 @@ export function mockAlmSettingsBindingStatus( ...overrides }; } + +export function mockProjectAlmBindingConfigurationErrors( + overrides: Partial<ProjectAlmBindingConfigurationErrors> = {} +): ProjectAlmBindingConfigurationErrors { + return { + scope: ProjectAlmBindingConfigurationErrorScope.Global, + errors: [{ msg: 'Foo bar is not correct' }, { msg: 'Bar baz has no permissions here' }], + ...overrides + }; +} |