aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2021-05-31 16:22:08 +0200
committersonartech <sonartech@sonarsource.com>2021-06-10 20:03:26 +0000
commit2902cdedeafb9803bc21464a4b50a05f2a7e0ded (patch)
treebdce466b066365ce8a5ca720fe7b6f13ca5a9148 /server
parentcac8b706867b5b56813df253516b2431d7f47889 (diff)
downloadsonarqube-2902cdedeafb9803bc21464a4b50a05f2a7e0ded.tar.gz
sonarqube-2902cdedeafb9803bc21464a4b50a05f2a7e0ded.zip
SONAR-14872 Display warning if PR deco cannot happen
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx197
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx39
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBgTaskNotif.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavProjectBindingErrorNotif.tsx96
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavProjectBindingErrorNotif-test.tsx76
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap341
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavProjectBindingErrorNotif-test.tsx.snap114
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts12
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
+ };
+}