Переглянути джерело

SONAR-14872 Display warning if PR deco cannot happen

tags/9.0.0.45539
Wouter Admiraal 3 роки тому
джерело
коміт
2902cdedea
14 змінених файлів з 762 додано та 179 видалено
  1. 107
    90
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  2. 32
    3
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  3. 34
    5
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  4. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx
  5. 96
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx
  6. 6
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  7. 76
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavProjectBindingErrorNotif-test.tsx
  8. 271
    70
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  9. 1
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap
  10. 114
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap
  11. 4
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  12. 3
    10
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx
  13. 12
    0
      server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
  14. 5
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 107
- 90
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)));

+ 32
- 3
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()}

+ 34
- 5
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

+ 1
- 1
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>
);

+ 96
- 0
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);

+ 6
- 0
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', () => {

+ 76
- 0
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}
/>
);
}

+ 271
- 70
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"

+ 1
- 0
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"
>

+ 114
- 0
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>
`;

+ 4
- 0
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;
}

+ 3
- 10
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();

+ 12
- 0
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
};
}

+ 5
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -2802,6 +2802,11 @@ component_navigation.status.last_blocked_due_to_bad_license_X=Last analysis bloc
component_navigation.last_analysis_had_warnings=Last analysis had {warnings}
component_navigation.x_warnings={warningsCount} {warningsCount, plural, one {warning} other {warnings}}

component_navigation.pr_deco.error_detected_X=We've detected an issue with your configuration. Your SonarQube instance won't be able to perform any pull request decoration. {action}
component_navigation.pr_deco.action.check_global_settings=Please check your global settings.
component_navigation.pr_deco.action.contact_sys_admin=Please contact your system administrator.
component_navigation.pr_deco.action.check_project_settings=Please check your project settings.
component_navigation.pr_deco.action.contact_project_admin=Please contact your project administrator.

background_task.status.ALL=All
background_task.status.PENDING=Pending

Завантаження…
Відмінити
Зберегти