]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13398 Display an additional message if some tasks are in error
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Thu, 18 Jun 2020 08:24:27 +0000 (10:24 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 26 Jun 2020 20:04:58 +0000 (20:04 +0000)
15 files changed:
server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationHelper-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap
server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationContext-test.tsx
server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx
server/sonar-web/src/main/js/types/indexation.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e22fb67eeef4a9ea8a3216fc96de63ffa2a73972..4483ae34af0205bd5be00f5d372de65477beeaa7 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+/* eslint-disable react/no-unused-state */
 import * as React from 'react';
 import { withAppState } from '../../../components/hoc/withAppState';
 import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation';
@@ -34,19 +35,13 @@ export class IndexationContextProvider extends React.PureComponent<
 > {
   mounted = false;
 
-  constructor(props: React.PropsWithChildren<Props>) {
-    super(props);
-
-    this.state = {
-      status: { isCompleted: !props.appState.needIssueSync }
-    };
-  }
-
   componentDidMount() {
     this.mounted = true;
 
-    if (!this.state.status.isCompleted) {
+    if (this.props.appState.needIssueSync) {
       IndexationNotificationHelper.startPolling(this.handleNewStatus);
+    } else {
+      this.setState({ status: { isCompleted: true, percentCompleted: 100, hasFailures: false } });
     }
   }
 
index e53b6e65257f398d911546230e3f833979a847bf..f83508fb0ca3b9b65da7e04fab24ca6411222dbe 100644 (file)
@@ -24,6 +24,7 @@ import withIndexationContext, {
   WithIndexationContextProps
 } from '../../../components/hoc/withIndexationContext';
 import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
+import { IndexationNotificationType } from '../../../types/indexation';
 import './IndexationNotification.css';
 import IndexationNotificationHelper from './IndexationNotificationHelper';
 import IndexationNotificationRenderer from './IndexationNotificationRenderer';
@@ -33,22 +34,16 @@ interface Props extends WithIndexationContextProps {
 }
 
 interface State {
-  progression?: IndexationProgression;
-}
-
-export enum IndexationProgression {
-  InProgress,
-  Completed
+  notificationType?: IndexationNotificationType;
 }
 
 export class IndexationNotification extends React.PureComponent<Props, State> {
-  state: State;
+  state: State = {};
   isSystemAdmin = false;
 
   constructor(props: Props) {
     super(props);
 
-    this.state = { progression: undefined };
     this.isSystemAdmin =
       isLoggedIn(this.props.currentUser) && hasGlobalPermission(this.props.currentUser, 'admin');
   }
@@ -57,42 +52,56 @@ export class IndexationNotification extends React.PureComponent<Props, State> {
     this.refreshNotification();
   }
 
-  componentDidUpdate() {
-    this.refreshNotification();
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.indexationContext.status !== this.props.indexationContext.status) {
+      this.refreshNotification();
+    }
   }
 
   refreshNotification() {
-    if (!this.props.indexationContext.status.isCompleted) {
+    const { isCompleted, hasFailures } = this.props.indexationContext.status;
+
+    if (!isCompleted) {
       IndexationNotificationHelper.markInProgressNotificationAsDisplayed();
-      this.setState({ progression: IndexationProgression.InProgress });
+      this.setState({
+        notificationType: hasFailures
+          ? IndexationNotificationType.InProgressWithFailure
+          : IndexationNotificationType.InProgress
+      });
+    } else if (hasFailures) {
+      this.setState({ notificationType: IndexationNotificationType.CompletedWithFailure });
     } else if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) {
-      this.setState({ progression: IndexationProgression.Completed });
+      this.setState({
+        notificationType: IndexationNotificationType.Completed
+      });
+    } else {
+      this.setState({ notificationType: undefined });
     }
   }
 
   handleDismissCompletedNotification = () => {
-    IndexationNotificationHelper.markCompletedNotificationAsDisplayed();
-    this.setState({ progression: undefined });
+    IndexationNotificationHelper.markCompletedNotificationAsDismissed();
+    this.refreshNotification();
   };
 
   render() {
-    const { progression } = this.state;
+    const { notificationType } = this.state;
     const {
       indexationContext: {
         status: { percentCompleted }
       }
     } = this.props;
 
-    if (progression === undefined) {
+    if (notificationType === undefined) {
       return null;
     }
 
     return (
       <IndexationNotificationRenderer
-        progression={progression}
-        percentCompleted={percentCompleted ?? 0}
+        type={notificationType}
+        percentCompleted={percentCompleted}
+        isSystemAdmin={this.isSystemAdmin}
         onDismissCompletedNotification={this.handleDismissCompletedNotification}
-        displayBackgroundTaskLink={this.isSystemAdmin}
       />
     );
   }
index 18e71d12b6c665d7da179fcb901c4f4a963dd6ae..514e766ce38dc35aee922e25c80d9dea9ae17120 100644 (file)
@@ -31,10 +31,10 @@ export default class IndexationNotificationHelper {
   static startPolling(onNewStatus: (status: IndexationStatus) => void) {
     this.stopPolling();
 
-    this.interval = setInterval(async () => {
-      const status = await getIndexationStatus();
-      onNewStatus(status);
-    }, POLLING_INTERVAL_MS);
+    // eslint-disable-next-line promise/catch-or-return
+    this.poll(onNewStatus).finally(() => {
+      this.interval = setInterval(() => this.poll(onNewStatus), POLLING_INTERVAL_MS);
+    });
   }
 
   static stopPolling() {
@@ -43,11 +43,17 @@ export default class IndexationNotificationHelper {
     }
   }
 
+  static async poll(onNewStatus: (status: IndexationStatus) => void) {
+    const status = await getIndexationStatus();
+
+    onNewStatus(status);
+  }
+
   static markInProgressNotificationAsDisplayed() {
     save(LS_INDEXATION_PROGRESS_WAS_DISPLAYED, true.toString());
   }
 
-  static markCompletedNotificationAsDisplayed() {
+  static markCompletedNotificationAsDismissed() {
     remove(LS_INDEXATION_PROGRESS_WAS_DISPLAYED);
   }
 
index 794786e793e1ff85d9860db4b1ce1269b09d2686..791059c3e90bd280dfd69b6cc0fa10cc5bb2e2cf 100644 (file)
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
-import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
+import { Alert, AlertProps } from 'sonar-ui-common/components/ui/Alert';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import { BackgroundTaskTypes } from '../../../apps/background-tasks/constants';
-import { IndexationProgression } from './IndexationNotification';
+import { BackgroundTaskTypes, STATUSES } from '../../../apps/background-tasks/constants';
+import { IndexationNotificationType } from '../../../types/indexation';
 
 export interface IndexationNotificationRendererProps {
-  progression: IndexationProgression;
+  type: IndexationNotificationType;
   percentCompleted: number;
+  isSystemAdmin: boolean;
   onDismissCompletedNotification: VoidFunction;
-  displayBackgroundTaskLink?: boolean;
 }
 
-export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) {
-  const { progression, percentCompleted, displayBackgroundTaskLink } = props;
+const NOTIFICATION_VARIANTS: { [key in IndexationNotificationType]: AlertProps['variant'] } = {
+  [IndexationNotificationType.InProgress]: 'warning',
+  [IndexationNotificationType.InProgressWithFailure]: 'error',
+  [IndexationNotificationType.Completed]: 'success',
+  [IndexationNotificationType.CompletedWithFailure]: 'error'
+};
 
-  const inProgress = progression === IndexationProgression.InProgress;
+export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) {
+  const { type } = props;
 
   return (
     <div className="indexation-notification-wrapper">
       <Alert
         className="indexation-notification-banner"
         display="banner"
-        variant={inProgress ? 'warning' : 'success'}>
+        variant={NOTIFICATION_VARIANTS[type]}>
         <div className="display-flex-center">
-          {inProgress ? (
-            <>
-              <span>{translate('indexation.in_progress')}</span>
-              <i className="spinner spacer-left" />
-              <span className="spacer-left">
-                {translateWithParameters('indexation.in_progress.details', percentCompleted)}
-              </span>
-              {displayBackgroundTaskLink && (
-                <span className="spacer-left">
-                  <FormattedMessage
-                    id="indexation.in_progress.admin_details"
-                    defaultMessage={translate('indexation.in_progress.admin_details')}
-                    values={{
-                      link: (
-                        <Link
-                          to={{
-                            pathname: '/admin/background_tasks',
-                            query: { taskType: BackgroundTaskTypes.IssueSync }
-                          }}>
-                          {translate('background_tasks.page')}
-                        </Link>
-                      )
-                    }}
-                  />
-                </span>
-              )}
-            </>
-          ) : (
-            <>
-              <span>{translate('indexation.completed')}</span>
-              <ButtonLink className="spacer-left" onClick={props.onDismissCompletedNotification}>
-                <strong>{translate('dismiss')}</strong>
-              </ButtonLink>
-            </>
-          )}
+          {type === IndexationNotificationType.Completed && renderCompletedBanner(props)}
+          {type === IndexationNotificationType.CompletedWithFailure &&
+            renderCompletedWithFailureBanner(props)}
+          {type === IndexationNotificationType.InProgress && renderInProgressBanner(props)}
+          {type === IndexationNotificationType.InProgressWithFailure &&
+            renderInProgressWithFailureBanner(props)}
         </div>
       </Alert>
     </div>
   );
 }
+
+function renderCompletedBanner(props: IndexationNotificationRendererProps) {
+  return (
+    <>
+      <span className="spacer-right">{translate('indexation.completed')}</span>
+      <ClearButton
+        className="button-tiny"
+        title={translate('dismiss')}
+        onClick={props.onDismissCompletedNotification}
+      />
+    </>
+  );
+}
+
+function renderCompletedWithFailureBanner(props: IndexationNotificationRendererProps) {
+  const { isSystemAdmin } = props;
+
+  return (
+    <span className="spacer-right">
+      <FormattedMessage
+        id="indexation.completed_with_error"
+        defaultMessage={translate('indexation.completed_with_error')}
+        values={{
+          link: isSystemAdmin
+            ? renderBackgroundTasksPageLink(true, translate('indexation.completed_with_error.link'))
+            : translate('indexation.completed_with_error.link')
+        }}
+      />
+    </span>
+  );
+}
+
+function renderInProgressBanner(props: IndexationNotificationRendererProps) {
+  const { percentCompleted, isSystemAdmin } = props;
+
+  return (
+    <>
+      <span className="spacer-right">{translate('indexation.in_progress')}</span>
+      <i className="spinner spacer-right" />
+      <span className="spacer-right">
+        {translateWithParameters('indexation.progression', percentCompleted)}
+      </span>
+      {isSystemAdmin && (
+        <span className="spacer-right">
+          <FormattedMessage
+            id="indexation.admin_link"
+            defaultMessage={translate('indexation.admin_link')}
+            values={{
+              link: renderBackgroundTasksPageLink(false, translate('background_tasks.page'))
+            }}
+          />
+        </span>
+      )}
+    </>
+  );
+}
+
+function renderInProgressWithFailureBanner(props: IndexationNotificationRendererProps) {
+  const { percentCompleted, isSystemAdmin } = props;
+
+  return (
+    <>
+      <span className="spacer-right">{translate('indexation.in_progress')}</span>
+      <i className="spinner spacer-right" />
+      <span className="spacer-right">
+        <FormattedMessage
+          id="indexation.progression_with_error"
+          defaultMessage={translateWithParameters(
+            'indexation.progression_with_error',
+            percentCompleted
+          )}
+          values={{
+            link: isSystemAdmin
+              ? renderBackgroundTasksPageLink(
+                  true,
+                  translate('indexation.progression_with_error.link')
+                )
+              : translate('indexation.progression_with_error.link')
+          }}
+        />
+      </span>
+    </>
+  );
+}
+
+function renderBackgroundTasksPageLink(hasError: boolean, text: string) {
+  return (
+    <Link
+      to={{
+        pathname: '/admin/background_tasks',
+        query: {
+          taskType: BackgroundTaskTypes.IssueSync,
+          status: hasError ? STATUSES.FAILED : undefined
+        }
+      }}>
+      {text}
+    </Link>
+  );
+}
index e0c8fb8130fa94d3a627a7abea2d29dec53e7157..d381912d60fb4929cdc3ea4d23b735a1410eb500 100644 (file)
@@ -29,26 +29,24 @@ beforeEach(() => jest.clearAllMocks());
 
 jest.mock('../IndexationNotificationHelper');
 
-it('should render correctly & start polling', () => {
+it('should render correctly and start polling if issue sync is needed', () => {
   const wrapper = mountRender();
 
-  expect(wrapper.state().status).toEqual({ isCompleted: false });
-
-  const child = wrapper.find(TestComponent);
-  expect(child.exists()).toBe(true);
-  expect(child.instance().context).toEqual(wrapper.state());
-});
-
-it('should start polling if needed', () => {
-  mountRender();
-
+  expect(wrapper).toMatchSnapshot();
   expect(IndexationNotificationHelper.startPolling).toHaveBeenCalled();
 });
 
-it('should not start polling if not needed', () => {
-  mountRender({ appState: { needIssueSync: false } });
+it('should not start polling if no issue sync is needed', () => {
+  const wrapper = mountRender({ appState: { needIssueSync: false } });
 
   expect(IndexationNotificationHelper.startPolling).not.toHaveBeenCalled();
+
+  const expectedStatus: IndexationStatus = {
+    isCompleted: true,
+    percentCompleted: 100,
+    hasFailures: false
+  };
+  expect(wrapper.state().status).toEqual(expectedStatus);
 });
 
 it('should update the state on new status & stop polling if indexation is complete', () => {
@@ -56,7 +54,11 @@ it('should update the state on new status & stop polling if indexation is comple
 
   const triggerNewStatus = (IndexationNotificationHelper.startPolling as jest.Mock).mock
     .calls[0][0] as (status: IndexationStatus) => void;
-  const newStatus = { isCompleted: true, percentCompleted: 100 };
+  const newStatus: IndexationStatus = {
+    isCompleted: true,
+    percentCompleted: 100,
+    hasFailures: false
+  };
 
   triggerNewStatus(newStatus);
 
index f09fc87d60a7c95abd8c92ee4d7ee33ef6638170..2efdb483485983a51c53cca704ebd0306caab65e 100644 (file)
@@ -21,7 +21,8 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockCurrentUser } from '../../../../helpers/testMocks';
-import { IndexationNotification, IndexationProgression } from '../IndexationNotification';
+import { IndexationNotificationType } from '../../../../types/indexation';
+import { IndexationNotification } from '../IndexationNotification';
 import IndexationNotificationHelper from '../IndexationNotificationHelper';
 import IndexationNotificationRenderer from '../IndexationNotificationRenderer';
 
@@ -29,63 +30,95 @@ beforeEach(() => jest.clearAllMocks());
 
 jest.mock('../IndexationNotificationHelper');
 
-it('should display the warning banner if indexation is in progress', () => {
-  const wrapper = shallowRender();
+describe('Completed banner', () => {
+  it('should be displayed', () => {
+    (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+      true
+    );
 
-  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled();
-  expect(wrapper.state().progression).toBe(IndexationProgression.InProgress);
-});
+    const wrapper = shallowRender();
 
-it('should display the success banner when indexation is complete', () => {
-  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
-    true
-  );
+    wrapper.setProps({
+      indexationContext: {
+        status: { isCompleted: true, percentCompleted: 100, hasFailures: false }
+      }
+    });
 
-  const wrapper = shallowRender();
+    expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
+    expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed);
+  });
 
-  wrapper.setProps({ indexationContext: { status: { isCompleted: true } } });
+  it('should be displayed at startup', () => {
+    (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+      true
+    );
 
-  expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
-  expect(wrapper.state().progression).toBe(IndexationProgression.Completed);
-});
+    const wrapper = shallowRender({
+      indexationContext: {
+        status: { isCompleted: true, percentCompleted: 100, hasFailures: false }
+      }
+    });
 
-it('should render correctly completed notification at startup', () => {
-  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
-    true
-  );
+    expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
+    expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed);
+  });
+
+  it('should be hidden on dismiss action', () => {
+    (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+      true
+    );
+
+    const wrapper = shallowRender({
+      indexationContext: {
+        status: { isCompleted: true, percentCompleted: 100, hasFailures: false }
+      }
+    });
+
+    expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed);
+
+    wrapper
+      .find(IndexationNotificationRenderer)
+      .props()
+      .onDismissCompletedNotification();
 
+    expect(IndexationNotificationHelper.markCompletedNotificationAsDismissed).toHaveBeenCalled();
+    expect(wrapper.state().notificationType).toBeUndefined();
+  });
+});
+
+it('should display the completed-with-failure banner', () => {
   const wrapper = shallowRender({
-    indexationContext: { status: { isCompleted: true } }
+    indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: true } }
   });
 
-  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).not.toHaveBeenCalled();
-  expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
-  expect(wrapper.state().progression).toBe(IndexationProgression.Completed);
+  expect(wrapper.state().notificationType).toBe(IndexationNotificationType.CompletedWithFailure);
 });
 
-it('should hide the success banner on dismiss action', () => {
-  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
-    true
-  );
-
+it('should display the progress banner', () => {
   const wrapper = shallowRender({
-    indexationContext: { status: { isCompleted: true } }
+    indexationContext: { status: { isCompleted: false, percentCompleted: 23, hasFailures: false } }
   });
 
-  wrapper
-    .find(IndexationNotificationRenderer)
-    .props()
-    .onDismissCompletedNotification();
+  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled();
+  expect(wrapper.state().notificationType).toBe(IndexationNotificationType.InProgress);
+});
 
-  expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled();
-  expect(wrapper.state().progression).toBeUndefined();
+it('should display the progress-with-failure banner', () => {
+  const wrapper = shallowRender({
+    indexationContext: { status: { isCompleted: false, percentCompleted: 23, hasFailures: true } }
+  });
+
+  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled();
+  expect(wrapper.state().notificationType).toBe(IndexationNotificationType.InProgressWithFailure);
 });
 
 function shallowRender(props?: Partial<IndexationNotification['props']>) {
   return shallow<IndexationNotification>(
     <IndexationNotification
       currentUser={mockCurrentUser()}
-      indexationContext={{ status: { isCompleted: false } }}
+      indexationContext={{
+        status: { isCompleted: false, percentCompleted: 23, hasFailures: false }
+      }}
       {...props}
     />
   );
index 6c3b7363e74fd8d6db335ce6dae77929d8847843..095d4b3194804c9b7afda9966a17481b4a9c1250 100644 (file)
@@ -40,17 +40,22 @@ jest.mock('sonar-ui-common/helpers/storage', () => ({
 
 it('should properly start & stop polling for indexation status', async () => {
   const onNewStatus = jest.fn();
-  const newStatus: IndexationStatus = { isCompleted: true, percentCompleted: 87 };
+  const newStatus: IndexationStatus = {
+    isCompleted: true,
+    percentCompleted: 100,
+    hasFailures: false
+  };
   (getIndexationStatus as jest.Mock).mockResolvedValueOnce(newStatus);
 
   IndexationNotificationHelper.startPolling(onNewStatus);
-
-  jest.runOnlyPendingTimers();
   expect(getIndexationStatus).toHaveBeenCalled();
 
   await new Promise(setImmediate);
   expect(onNewStatus).toHaveBeenCalledWith(newStatus);
 
+  jest.runOnlyPendingTimers();
+  expect(getIndexationStatus).toHaveBeenCalledTimes(2);
+
   (getIndexationStatus as jest.Mock).mockClear();
 
   IndexationNotificationHelper.stopPolling();
@@ -70,7 +75,7 @@ it('should properly handle the flag to show the completed banner', () => {
   expect(shouldDisplay).toBe(true);
   expect(get).toHaveBeenCalled();
 
-  IndexationNotificationHelper.markCompletedNotificationAsDisplayed();
+  IndexationNotificationHelper.markCompletedNotificationAsDismissed();
 
   expect(remove).toHaveBeenCalled();
 
index ace5c8ee506f9f5b255fc957009ea16f50415037..ea4f06dbb921d213d323fe111ddfc117d6dc9194 100644 (file)
 
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
 import { click } from 'sonar-ui-common/helpers/testUtils';
-import { IndexationProgression } from '../IndexationNotification';
+import { IndexationNotificationType } from '../../../../types/indexation';
 import IndexationNotificationRenderer, {
   IndexationNotificationRendererProps
 } from '../IndexationNotificationRenderer';
 
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('in-progress');
-  expect(shallowRender({ displayBackgroundTaskLink: true })).toMatchSnapshot('in-progress-admin');
-  expect(shallowRender({ progression: IndexationProgression.Completed })).toMatchSnapshot(
-    'completed'
-  );
-});
+it.each([
+  [IndexationNotificationType.InProgress, false],
+  [IndexationNotificationType.InProgress, true],
+  [IndexationNotificationType.InProgressWithFailure, false],
+  [IndexationNotificationType.InProgressWithFailure, true],
+  [IndexationNotificationType.Completed, false],
+  [IndexationNotificationType.Completed, true],
+  [IndexationNotificationType.CompletedWithFailure, false],
+  [IndexationNotificationType.CompletedWithFailure, true]
+])(
+  'should render correctly for type=%p & isSystemAdmin=%p',
+  (type: IndexationNotificationType, isSystemAdmin: boolean) => {
+    expect(shallowRender({ type, isSystemAdmin })).toMatchSnapshot();
+  }
+);
 
-it('should propagate the dismiss event', () => {
+it('should propagate the dismiss event from completed notification', () => {
   const onDismissCompletedNotification = jest.fn();
   const wrapper = shallowRender({
-    progression: IndexationProgression.Completed,
+    type: IndexationNotificationType.Completed,
     onDismissCompletedNotification
   });
 
-  click(wrapper.find(ButtonLink));
+  click(wrapper.find(ClearButton));
   expect(onDismissCompletedNotification).toHaveBeenCalled();
 });
 
 function shallowRender(props: Partial<IndexationNotificationRendererProps> = {}) {
   return shallow<IndexationNotificationRendererProps>(
     <IndexationNotificationRenderer
-      progression={IndexationProgression.InProgress}
+      type={IndexationNotificationType.InProgress}
       percentCompleted={25}
+      isSystemAdmin={false}
       onDismissCompletedNotification={jest.fn()}
       {...props}
     />
index d988c8764dc515e34ed0559909a3cf8cc62553f5..9d7f1945d4d12a06494a8f342a764134932dcb14 100644 (file)
@@ -47,7 +47,7 @@ function shallowRender(props?: PageUnavailableDueToIndexation['props']) {
   return shallow(
     <PageUnavailableDueToIndexation
       indexationContext={{
-        status: { isCompleted: false }
+        status: { isCompleted: false, percentCompleted: 23, hasFailures: false }
       }}
       pageContext={PageContext.Issues}
       component={{ qualifier: ComponentQualifier.Portfolio, name: 'test-portfolio' }}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationContextProvider-test.tsx.snap
new file mode 100644 (file)
index 0000000..a7c918f
--- /dev/null
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly and start polling if issue sync is needed 1`] = `
+<IndexationContextProvider
+  appState={
+    Object {
+      "needIssueSync": true,
+    }
+  }
+>
+  <TestComponent>
+    <h1>
+      TestComponent
+    </h1>
+  </TestComponent>
+</IndexationContextProvider>
+`;
index 2674a8f4a124347c7c26a6b1cc8940c540ce26bf..8c57b55f88ad3a57d2deb7961cdbae5be4c46f0d 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render correctly: completed 1`] = `
+exports[`should render correctly for type="Completed" & isSystemAdmin=false 1`] = `
 <div
   className="indexation-notification-wrapper"
 >
@@ -12,23 +12,123 @@ exports[`should render correctly: completed 1`] = `
     <div
       className="display-flex-center"
     >
-      <span>
+      <span
+        className="spacer-right"
+      >
         indexation.completed
       </span>
-      <ButtonLink
-        className="spacer-left"
+      <ClearButton
+        className="button-tiny"
         onClick={[MockFunction]}
+        title="dismiss"
+      />
+    </div>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly for type="Completed" & isSystemAdmin=true 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="success"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="spacer-right"
+      >
+        indexation.completed
+      </span>
+      <ClearButton
+        className="button-tiny"
+        onClick={[MockFunction]}
+        title="dismiss"
+      />
+    </div>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin=false 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="error"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="spacer-right"
       >
-        <strong>
-          dismiss
-        </strong>
-      </ButtonLink>
+        <FormattedMessage
+          defaultMessage="indexation.completed_with_error"
+          id="indexation.completed_with_error"
+          values={
+            Object {
+              "link": "indexation.completed_with_error.link",
+            }
+          }
+        />
+      </span>
+    </div>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin=true 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="error"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="spacer-right"
+      >
+        <FormattedMessage
+          defaultMessage="indexation.completed_with_error"
+          id="indexation.completed_with_error"
+          values={
+            Object {
+              "link": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to={
+                  Object {
+                    "pathname": "/admin/background_tasks",
+                    "query": Object {
+                      "status": "FAILED",
+                      "taskType": "ISSUE_SYNC",
+                    },
+                  }
+                }
+              >
+                indexation.completed_with_error.link
+              </Link>,
+            }
+          }
+        />
+      </span>
     </div>
   </Alert>
 </div>
 `;
 
-exports[`should render correctly: in-progress 1`] = `
+exports[`should render correctly for type="InProgress" & isSystemAdmin=false 1`] = `
 <div
   className="indexation-notification-wrapper"
 >
@@ -40,23 +140,25 @@ exports[`should render correctly: in-progress 1`] = `
     <div
       className="display-flex-center"
     >
-      <span>
+      <span
+        className="spacer-right"
+      >
         indexation.in_progress
       </span>
       <i
-        className="spinner spacer-left"
+        className="spinner spacer-right"
       />
       <span
-        className="spacer-left"
+        className="spacer-right"
       >
-        indexation.in_progress.details.25
+        indexation.progression.25
       </span>
     </div>
   </Alert>
 </div>
 `;
 
-exports[`should render correctly: in-progress-admin 1`] = `
+exports[`should render correctly for type="InProgress" & isSystemAdmin=true 1`] = `
 <div
   className="indexation-notification-wrapper"
 >
@@ -68,23 +170,25 @@ exports[`should render correctly: in-progress-admin 1`] = `
     <div
       className="display-flex-center"
     >
-      <span>
+      <span
+        className="spacer-right"
+      >
         indexation.in_progress
       </span>
       <i
-        className="spinner spacer-left"
+        className="spinner spacer-right"
       />
       <span
-        className="spacer-left"
+        className="spacer-right"
       >
-        indexation.in_progress.details.25
+        indexation.progression.25
       </span>
       <span
-        className="spacer-left"
+        className="spacer-right"
       >
         <FormattedMessage
-          defaultMessage="indexation.in_progress.admin_details"
-          id="indexation.in_progress.admin_details"
+          defaultMessage="indexation.admin_link"
+          id="indexation.admin_link"
           values={
             Object {
               "link": <Link
@@ -94,6 +198,7 @@ exports[`should render correctly: in-progress-admin 1`] = `
                   Object {
                     "pathname": "/admin/background_tasks",
                     "query": Object {
+                      "status": undefined,
                       "taskType": "ISSUE_SYNC",
                     },
                   }
@@ -109,3 +214,93 @@ exports[`should render correctly: in-progress-admin 1`] = `
   </Alert>
 </div>
 `;
+
+exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmin=false 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="error"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="spacer-right"
+      >
+        indexation.in_progress
+      </span>
+      <i
+        className="spinner spacer-right"
+      />
+      <span
+        className="spacer-right"
+      >
+        <FormattedMessage
+          defaultMessage="indexation.progression_with_error.25"
+          id="indexation.progression_with_error"
+          values={
+            Object {
+              "link": "indexation.progression_with_error.link",
+            }
+          }
+        />
+      </span>
+    </div>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmin=true 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="error"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="spacer-right"
+      >
+        indexation.in_progress
+      </span>
+      <i
+        className="spinner spacer-right"
+      />
+      <span
+        className="spacer-right"
+      >
+        <FormattedMessage
+          defaultMessage="indexation.progression_with_error.25"
+          id="indexation.progression_with_error"
+          values={
+            Object {
+              "link": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to={
+                  Object {
+                    "pathname": "/admin/background_tasks",
+                    "query": Object {
+                      "status": "FAILED",
+                      "taskType": "ISSUE_SYNC",
+                    },
+                  }
+                }
+              >
+                indexation.progression_with_error.link
+              </Link>,
+            }
+          }
+        />
+      </span>
+    </div>
+  </Alert>
+</div>
+`;
index d4ca9f9452f697b37cd7f5d71065b48a0b6bbe02..3e2a264f5adfcd5ba9b6b7525c131832b4897f45 100644 (file)
@@ -26,7 +26,7 @@ import withIndexationContext, { WithIndexationContextProps } from '../withIndexa
 
 it('should render correctly', () => {
   const indexationContext: IndexationContextInterface = {
-    status: { isCompleted: true, percentCompleted: 87 }
+    status: { isCompleted: true, percentCompleted: 87, hasFailures: false }
   };
 
   const wrapper = mountRender(indexationContext);
@@ -36,7 +36,11 @@ it('should render correctly', () => {
 
 function mountRender(indexationContext?: Partial<IndexationContextInterface>) {
   return mount(
-    <IndexationContext.Provider value={{ status: { isCompleted: false }, ...indexationContext }}>
+    <IndexationContext.Provider
+      value={{
+        status: { isCompleted: false, percentCompleted: 23, hasFailures: false },
+        ...indexationContext
+      }}>
       <TestComponentWithIndexationContext />
     </IndexationContext.Provider>
   );
index fca47cf1b8d53881532ad5d5e287eac3f5e719c2..c2f4a1bc42ade0f95414d0402d0ff0c8a451b62b 100644 (file)
@@ -29,13 +29,19 @@ it('should render correctly', () => {
   let wrapper = mountRender();
   expect(wrapper.find(TestComponent).exists()).toBe(false);
 
-  wrapper = mountRender({ status: { isCompleted: true } });
+  wrapper = mountRender({
+    status: { isCompleted: true, percentCompleted: 100, hasFailures: false }
+  });
   expect(wrapper.find(TestComponent).exists()).toBe(true);
 });
 
 function mountRender(context?: Partial<IndexationContextInterface>) {
   return mount(
-    <IndexationContext.Provider value={{ status: { isCompleted: false }, ...context }}>
+    <IndexationContext.Provider
+      value={{
+        status: { isCompleted: false, percentCompleted: 23, hasFailures: false },
+        ...context
+      }}>
       <TestComponentWithGuard />
     </IndexationContext.Provider>
   );
index 28f9b6eba5378882d352d618163683646551d518..7ec70554bd1152d3f7ef1d3c4a790d8e4e0d17fb 100644 (file)
 
 export interface IndexationStatus {
   isCompleted: boolean;
-  percentCompleted?: number;
+  percentCompleted: number;
+  hasFailures: boolean;
 }
 
 export interface IndexationContextInterface {
   status: IndexationStatus;
 }
+
+export enum IndexationNotificationType {
+  InProgress = 'InProgress',
+  InProgressWithFailure = 'InProgressWithFailure',
+  Completed = 'Completed',
+  CompletedWithFailure = 'CompletedWithFailure'
+}
index 97bd3af3747ec5465782d379fa9ad73a0696c564..5aafe022028e3a6a83c5da6671a9bddfc04bfd05 100644 (file)
@@ -3563,9 +3563,13 @@ maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Pleas
 #
 #------------------------------------------------------------------------------
 indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete. 
-indexation.in_progress.details={0}% completed.
-indexation.in_progress.admin_details=See {link}.
+indexation.progression={0}% complete.
+indexation.progression_with_error={0}% complete with some {link}.
+indexation.progression_with_error.link=tasks failing
 indexation.completed=All project data has been reloaded.
+indexation.completed_with_error=SonarQube completed the reload of project data. Some {link} causing some projects to remain unavailable.
+indexation.completed_with_error.link=tasks failed
+indexation.admin_link=See {link} for more information.
 indexation.page_unavailable.title.issues=Issues page is temporarily unavailable
 indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unavailable
 indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable