]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13398 Display an alert with the indexation status
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 3 Jun 2020 17:23:52 +0000 (19:23 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 26 Jun 2020 20:04:58 +0000 (20:04 +0000)
20 files changed:
server/sonar-web/src/main/js/api/ce.ts
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
server/sonar-web/src/main/js/app/components/indexation/IndexationContext.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.css [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationHelper-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationContext-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/withIndexationContext.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/types/indexation.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/types.d.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8b90d9642af7b828e7b82623e57e9ca166b0b995..a4d1a03a5ca2eba5dd52595356e6e1b32cd445d2 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { getJSON, post, RequestData } from 'sonar-ui-common/helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
+import { IndexationStatus } from '../types/indexation';
 
 export function getAnalysisStatus(data: {
   component: string;
@@ -83,3 +84,7 @@ export function getWorkers(): Promise<{ canSetWorkerCount: boolean; value: numbe
 export function setWorkerCount(count: number): Promise<void | Response> {
   return post('/api/ce/set_worker_count', { count }).catch(throwGlobalError);
 }
+
+export function getIndexationStatus(): Promise<IndexationStatus> {
+  return getJSON('/api/ce/indexation_status').catch(throwGlobalError);
+}
index 4ea0a702a67703c708686120f4d30482de592f89..92e728746268f058b59a0e842b8ce9d8a61eb033 100644 (file)
@@ -24,6 +24,8 @@ import A11ySkipLinks from './a11y/A11ySkipLinks';
 import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider';
 import GlobalFooterContainer from './GlobalFooterContainer';
 import GlobalMessagesContainer from './GlobalMessagesContainer';
+import IndexationContextProvider from './indexation/IndexationContextProvider';
+import IndexationNotification from './indexation/IndexationNotification';
 import GlobalNav from './nav/global/GlobalNav';
 import StartupModal from './StartupModal';
 
@@ -46,9 +48,12 @@ export default function GlobalContainer(props: Props) {
             <div className="page-wrapper" id="container">
               <div className="page-container">
                 <Workspace>
-                  <GlobalNav location={props.location} />
-                  <GlobalMessagesContainer />
-                  {props.children}
+                  <IndexationContextProvider>
+                    <GlobalNav location={props.location} />
+                    <GlobalMessagesContainer />
+                    <IndexationNotification />
+                    {props.children}
+                  </IndexationContextProvider>
                 </Workspace>
               </div>
             </div>
index 5d4c5d631f298197288ecdfd609dd663dabcbc90..adf8cc40e4721a800b3fc50c5c020d6cc2c4f7af 100644 (file)
@@ -16,21 +16,24 @@ exports[`should render correctly 1`] = `
             className="page-container"
           >
             <Workspace>
-              <Connect(GlobalNav)
-                location={
-                  Object {
-                    "action": "PUSH",
-                    "hash": "",
-                    "key": "key",
-                    "pathname": "/path",
-                    "query": Object {},
-                    "search": "",
-                    "state": Object {},
+              <Connect(withAppState(IndexationContextProvider))>
+                <Connect(GlobalNav)
+                  location={
+                    Object {
+                      "action": "PUSH",
+                      "hash": "",
+                      "key": "key",
+                      "pathname": "/path",
+                      "query": Object {},
+                      "search": "",
+                      "state": Object {},
+                    }
                   }
-                }
-              />
-              <Connect(GlobalMessages) />
-              <ChildComponent />
+                />
+                <Connect(GlobalMessages) />
+                <withIndexationContext(IndexationNotification) />
+                <ChildComponent />
+              </Connect(withAppState(IndexationContextProvider))>
             </Workspace>
           </div>
         </div>
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationContext.ts b/server/sonar-web/src/main/js/app/components/indexation/IndexationContext.ts
new file mode 100644 (file)
index 0000000..1e8793d
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { createContext } from 'react';
+import { IndexationContextInterface } from '../../../types/indexation';
+
+// eslint-disable-next-line import/prefer-default-export
+export const IndexationContext = createContext<IndexationContextInterface | null>(null);
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationContextProvider.tsx
new file mode 100644 (file)
index 0000000..e22fb67
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { withAppState } from '../../../components/hoc/withAppState';
+import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation';
+import { IndexationContext } from './IndexationContext';
+import IndexationNotificationHelper from './IndexationNotificationHelper';
+
+interface Props {
+  appState: Pick<T.AppState, 'needIssueSync'>;
+}
+
+export class IndexationContextProvider extends React.PureComponent<
+  React.PropsWithChildren<Props>,
+  IndexationContextInterface
+> {
+  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) {
+      IndexationNotificationHelper.startPolling(this.handleNewStatus);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+
+    IndexationNotificationHelper.stopPolling();
+  }
+
+  handleNewStatus = (newIndexationStatus: IndexationStatus) => {
+    if (newIndexationStatus.isCompleted) {
+      IndexationNotificationHelper.stopPolling();
+    }
+
+    if (this.mounted) {
+      this.setState({ status: newIndexationStatus });
+    }
+  };
+
+  render() {
+    return (
+      <IndexationContext.Provider value={this.state}>
+        {this.props.children}
+      </IndexationContext.Provider>
+    );
+  }
+}
+
+export default withAppState(IndexationContextProvider);
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.css b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.css
new file mode 100644 (file)
index 0000000..b69f709
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+.indexation-notification-wrapper {
+  height: 34px;
+}
+
+.indexation-notification-banner {
+  position: fixed;
+  width: 100%;
+  z-index: var(--globalBannerZIndex);
+  margin-bottom: 0 !important;
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
new file mode 100644 (file)
index 0000000..50c9818
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 withIndexationContext, {
+  WithIndexationContextProps
+} from '../../../components/hoc/withIndexationContext';
+import './IndexationNotification.css';
+import IndexationNotificationHelper from './IndexationNotificationHelper';
+import IndexationNotificationRenderer from './IndexationNotificationRenderer';
+
+interface State {
+  progression?: IndexationProgression;
+}
+
+export enum IndexationProgression {
+  InProgress,
+  Completed
+}
+
+export class IndexationNotification extends React.PureComponent<WithIndexationContextProps, State> {
+  state: State = {
+    progression: undefined
+  };
+
+  componentDidMount() {
+    this.refreshNotification();
+  }
+
+  componentDidUpdate() {
+    this.refreshNotification();
+  }
+
+  refreshNotification() {
+    if (!this.props.indexationContext.status.isCompleted) {
+      IndexationNotificationHelper.markInProgressNotificationAsDisplayed();
+      this.setState({ progression: IndexationProgression.InProgress });
+    } else if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) {
+      this.setState({ progression: IndexationProgression.Completed });
+    }
+  }
+
+  handleDismissCompletedNotification = () => {
+    IndexationNotificationHelper.markCompletedNotificationAsDisplayed();
+    this.setState({ progression: undefined });
+  };
+
+  render() {
+    const { progression } = this.state;
+    const {
+      indexationContext: {
+        status: { percentCompleted }
+      }
+    } = this.props;
+
+    if (progression === undefined) {
+      return null;
+    }
+
+    return (
+      <IndexationNotificationRenderer
+        progression={progression}
+        percentCompleted={percentCompleted ?? 0}
+        onDismissCompletedNotification={this.handleDismissCompletedNotification}
+      />
+    );
+  }
+}
+
+export default withIndexationContext(IndexationNotification);
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts
new file mode 100644 (file)
index 0000000..18e71d1
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { get, remove, save } from 'sonar-ui-common/helpers/storage';
+import { getIndexationStatus } from '../../../api/ce';
+import { IndexationStatus } from '../../../types/indexation';
+
+const POLLING_INTERVAL_MS = 5000;
+const LS_INDEXATION_PROGRESS_WAS_DISPLAYED = 'indexation.progress.was.displayed';
+
+export default class IndexationNotificationHelper {
+  private static interval?: NodeJS.Timeout;
+
+  static startPolling(onNewStatus: (status: IndexationStatus) => void) {
+    this.stopPolling();
+
+    this.interval = setInterval(async () => {
+      const status = await getIndexationStatus();
+      onNewStatus(status);
+    }, POLLING_INTERVAL_MS);
+  }
+
+  static stopPolling() {
+    if (this.interval) {
+      clearInterval(this.interval);
+    }
+  }
+
+  static markInProgressNotificationAsDisplayed() {
+    save(LS_INDEXATION_PROGRESS_WAS_DISPLAYED, true.toString());
+  }
+
+  static markCompletedNotificationAsDisplayed() {
+    remove(LS_INDEXATION_PROGRESS_WAS_DISPLAYED);
+  }
+
+  static shouldDisplayCompletedNotification() {
+    return JSON.parse(get(LS_INDEXATION_PROGRESS_WAS_DISPLAYED) || false.toString());
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx
new file mode 100644 (file)
index 0000000..9cb464c
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { IndexationProgression } from './IndexationNotification';
+
+export interface IndexationNotificationRendererProps {
+  progression: IndexationProgression;
+  percentCompleted: number;
+  onDismissCompletedNotification: VoidFunction;
+}
+
+export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) {
+  const { progression, percentCompleted } = props;
+
+  const inProgress = progression === IndexationProgression.InProgress;
+
+  return (
+    <div className="indexation-notification-wrapper">
+      <Alert
+        className="indexation-notification-banner"
+        display="banner"
+        variant={inProgress ? 'warning' : 'success'}>
+        <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>
+            </>
+          ) : (
+            <>
+              <span>{translate('indexation.completed')}</span>
+              <ButtonLink className="spacer-left" onClick={props.onDismissCompletedNotification}>
+                <strong>{translate('dismiss')}</strong>
+              </ButtonLink>
+            </>
+          )}
+        </div>
+      </Alert>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationContextProvider-test.tsx
new file mode 100644 (file)
index 0000000..e0c8fb8
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { mount } from 'enzyme';
+import * as React from 'react';
+import { IndexationStatus } from '../../../../types/indexation';
+import { IndexationContext } from '../IndexationContext';
+import { IndexationContextProvider } from '../IndexationContextProvider';
+import IndexationNotificationHelper from '../IndexationNotificationHelper';
+
+beforeEach(() => jest.clearAllMocks());
+
+jest.mock('../IndexationNotificationHelper');
+
+it('should render correctly & start polling', () => {
+  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(IndexationNotificationHelper.startPolling).toHaveBeenCalled();
+});
+
+it('should not start polling if not needed', () => {
+  mountRender({ appState: { needIssueSync: false } });
+
+  expect(IndexationNotificationHelper.startPolling).not.toHaveBeenCalled();
+});
+
+it('should update the state on new status & stop polling if indexation is complete', () => {
+  const wrapper = mountRender();
+
+  const triggerNewStatus = (IndexationNotificationHelper.startPolling as jest.Mock).mock
+    .calls[0][0] as (status: IndexationStatus) => void;
+  const newStatus = { isCompleted: true, percentCompleted: 100 };
+
+  triggerNewStatus(newStatus);
+
+  expect(wrapper.state().status).toEqual(newStatus);
+  expect(IndexationNotificationHelper.stopPolling).toHaveBeenCalled();
+});
+
+it('should stop polling when component is destroyed', () => {
+  const wrapper = mountRender();
+
+  wrapper.unmount();
+
+  expect(IndexationNotificationHelper.stopPolling).toHaveBeenCalled();
+});
+
+function mountRender(props?: IndexationContextProvider['props']) {
+  return mount<IndexationContextProvider>(
+    <IndexationContextProvider appState={{ needIssueSync: true }} {...props}>
+      <TestComponent />
+    </IndexationContextProvider>
+  );
+}
+
+class TestComponent extends React.PureComponent {
+  context!: IndexationStatus;
+  static contextType = IndexationContext;
+
+  render() {
+    return <h1>TestComponent</h1>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx
new file mode 100644 (file)
index 0000000..207db1e
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { IndexationNotification, IndexationProgression } from '../IndexationNotification';
+import IndexationNotificationHelper from '../IndexationNotificationHelper';
+import IndexationNotificationRenderer from '../IndexationNotificationRenderer';
+
+beforeEach(() => jest.clearAllMocks());
+
+jest.mock('../IndexationNotificationHelper');
+
+it('should display the warning banner if indexation is in progress', () => {
+  const wrapper = shallowRender();
+
+  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled();
+  expect(wrapper.state().progression).toBe(IndexationProgression.InProgress);
+});
+
+it('should display the success banner when indexation is complete', () => {
+  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+    true
+  );
+
+  const wrapper = shallowRender();
+
+  wrapper.setProps({ indexationContext: { status: { isCompleted: true } } });
+
+  expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
+  expect(wrapper.state().progression).toBe(IndexationProgression.Completed);
+});
+
+it('should render correctly completed notification at startup', () => {
+  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+    true
+  );
+
+  const wrapper = shallowRender({
+    indexationContext: { status: { isCompleted: true } }
+  });
+
+  expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).not.toHaveBeenCalled();
+  expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled();
+  expect(wrapper.state().progression).toBe(IndexationProgression.Completed);
+});
+
+it('should hide the success banner on dismiss action', () => {
+  (IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce(
+    true
+  );
+
+  const wrapper = shallowRender({
+    indexationContext: { status: { isCompleted: true } }
+  });
+
+  wrapper
+    .find(IndexationNotificationRenderer)
+    .props()
+    .onDismissCompletedNotification();
+
+  expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled();
+  expect(wrapper.state().progression).toBeUndefined();
+});
+
+function shallowRender(props?: Partial<IndexationNotification['props']>) {
+  return shallow<IndexationNotification>(
+    <IndexationNotification indexationContext={{ status: { isCompleted: false } }} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationHelper-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationHelper-test.tsx
new file mode 100644 (file)
index 0000000..6c3b736
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { get, remove, save } from 'sonar-ui-common/helpers/storage';
+import { getIndexationStatus } from '../../../../api/ce';
+import { IndexationStatus } from '../../../../types/indexation';
+import IndexationNotificationHelper from '../IndexationNotificationHelper';
+
+beforeEach(() => {
+  jest.clearAllMocks();
+  jest.useFakeTimers();
+});
+
+jest.mock('../../../../api/ce', () => ({
+  getIndexationStatus: jest.fn()
+}));
+
+jest.mock('sonar-ui-common/helpers/storage', () => ({
+  get: jest.fn(),
+  remove: jest.fn(),
+  save: jest.fn()
+}));
+
+it('should properly start & stop polling for indexation status', async () => {
+  const onNewStatus = jest.fn();
+  const newStatus: IndexationStatus = { isCompleted: true, percentCompleted: 87 };
+  (getIndexationStatus as jest.Mock).mockResolvedValueOnce(newStatus);
+
+  IndexationNotificationHelper.startPolling(onNewStatus);
+
+  jest.runOnlyPendingTimers();
+  expect(getIndexationStatus).toHaveBeenCalled();
+
+  await new Promise(setImmediate);
+  expect(onNewStatus).toHaveBeenCalledWith(newStatus);
+
+  (getIndexationStatus as jest.Mock).mockClear();
+
+  IndexationNotificationHelper.stopPolling();
+  jest.runAllTimers();
+
+  expect(getIndexationStatus).not.toHaveBeenCalled();
+});
+
+it('should properly handle the flag to show the completed banner', () => {
+  IndexationNotificationHelper.markInProgressNotificationAsDisplayed();
+
+  expect(save).toHaveBeenCalledWith(expect.any(String), 'true');
+
+  (get as jest.Mock).mockReturnValueOnce('true');
+  let shouldDisplay = IndexationNotificationHelper.shouldDisplayCompletedNotification();
+
+  expect(shouldDisplay).toBe(true);
+  expect(get).toHaveBeenCalled();
+
+  IndexationNotificationHelper.markCompletedNotificationAsDisplayed();
+
+  expect(remove).toHaveBeenCalled();
+
+  shouldDisplay = IndexationNotificationHelper.shouldDisplayCompletedNotification();
+
+  expect(shouldDisplay).toBe(false);
+});
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx
new file mode 100644 (file)
index 0000000..3bafb5e
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { IndexationProgression } from '../IndexationNotification';
+import IndexationNotificationRenderer, {
+  IndexationNotificationRendererProps
+} from '../IndexationNotificationRenderer';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('in-progress');
+  expect(shallowRender({ progression: IndexationProgression.Completed })).toMatchSnapshot(
+    'completed'
+  );
+});
+
+it('should propagate the dismiss event', () => {
+  const onDismissCompletedNotification = jest.fn();
+  const wrapper = shallowRender({
+    progression: IndexationProgression.Completed,
+    onDismissCompletedNotification
+  });
+
+  click(wrapper.find(ButtonLink));
+  expect(onDismissCompletedNotification).toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<IndexationNotificationRendererProps> = {}) {
+  return shallow<IndexationNotificationRendererProps>(
+    <IndexationNotificationRenderer
+      progression={IndexationProgression.InProgress}
+      percentCompleted={25}
+      onDismissCompletedNotification={jest.fn()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/IndexationNotificationRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..c0cb064
--- /dev/null
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: completed 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="success"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span>
+        indexation.completed
+      </span>
+      <ButtonLink
+        className="spacer-left"
+        onClick={[MockFunction]}
+      >
+        <strong>
+          dismiss
+        </strong>
+      </ButtonLink>
+    </div>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly: in-progress 1`] = `
+<div
+  className="indexation-notification-wrapper"
+>
+  <Alert
+    className="indexation-notification-banner"
+    display="banner"
+    variant="warning"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span>
+        indexation.in_progress
+      </span>
+      <i
+        className="spinner spacer-left"
+      />
+      <span
+        className="spacer-left"
+      >
+        indexation.in_progress.details.25
+      </span>
+    </div>
+  </Alert>
+</div>
+`;
index e5aa510ffc6da8779f95e8935de45135f8cff0b8..548cbc3a042cd693b125bf33c2c1584d5100b66b 100644 (file)
@@ -193,6 +193,8 @@ module.exports = {
     pageMainZIndex: '50',
     pageSideZIndex: '51',
 
+    globalBannerZIndex: '60',
+
     tooltipZIndex: '8000',
 
     dropdownMenuZIndex: '7500',
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationContext-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationContext-test.tsx
new file mode 100644 (file)
index 0000000..d4ca9f9
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { mount } from 'enzyme';
+import * as React from 'react';
+import { IndexationContext } from '../../../app/components/indexation/IndexationContext';
+import { IndexationContextInterface } from '../../../types/indexation';
+import withIndexationContext, { WithIndexationContextProps } from '../withIndexationContext';
+
+it('should render correctly', () => {
+  const indexationContext: IndexationContextInterface = {
+    status: { isCompleted: true, percentCompleted: 87 }
+  };
+
+  const wrapper = mountRender(indexationContext);
+
+  expect(wrapper.find(TestComponent).props().indexationContext).toEqual(indexationContext);
+});
+
+function mountRender(indexationContext?: Partial<IndexationContextInterface>) {
+  return mount(
+    <IndexationContext.Provider value={{ status: { isCompleted: false }, ...indexationContext }}>
+      <TestComponentWithIndexationContext />
+    </IndexationContext.Provider>
+  );
+}
+
+class TestComponent extends React.PureComponent<WithIndexationContextProps> {
+  render() {
+    return <h1>TestComponent</h1>;
+  }
+}
+
+const TestComponentWithIndexationContext = withIndexationContext(TestComponent);
diff --git a/server/sonar-web/src/main/js/components/hoc/withIndexationContext.tsx b/server/sonar-web/src/main/js/components/hoc/withIndexationContext.tsx
new file mode 100644 (file)
index 0000000..a948a26
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { IndexationContext } from '../../app/components/indexation/IndexationContext';
+import { IndexationContextInterface } from '../../types/indexation';
+import { getWrappedDisplayName } from './utils';
+
+export interface WithIndexationContextProps {
+  indexationContext: IndexationContextInterface;
+}
+
+export default function withIndexationContext<P>(
+  WrappedComponent: React.ComponentType<P & WithIndexationContextProps>
+) {
+  return class WithIndexationContext extends React.PureComponent<
+    Omit<P, keyof WithIndexationContextProps>
+  > {
+    static displayName = getWrappedDisplayName(WrappedComponent, 'withIndexationContext');
+
+    render() {
+      return (
+        <IndexationContext.Consumer>
+          {indexationContext => {
+            if (indexationContext) {
+              return (
+                <WrappedComponent indexationContext={indexationContext} {...(this.props as P)} />
+              );
+            }
+
+            return null;
+          }}
+        </IndexationContext.Consumer>
+      );
+    }
+  };
+}
diff --git a/server/sonar-web/src/main/js/types/indexation.ts b/server/sonar-web/src/main/js/types/indexation.ts
new file mode 100644 (file)
index 0000000..28f9b6e
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+export interface IndexationStatus {
+  isCompleted: boolean;
+  percentCompleted?: number;
+}
+
+export interface IndexationContextInterface {
+  status: IndexationStatus;
+}
index df78c685b06419929ec47deccdbabf433cef9730..72b81c469c00dd4e555454805dfff87e004225ba 100644 (file)
@@ -100,6 +100,7 @@ declare namespace T {
     edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined;
     globalPages?: Extension[];
     multipleAlmEnabled?: boolean;
+    needIssueSync?: boolean;
     organizationsEnabled?: boolean;
     productionDatabase: boolean;
     qualifiers: string[];
index 2849bee64c6c66f6dbc8d3dc9b67e5f95e04b66c..4cf2149df4d9119247a3906ec79772374dcb9e80 100644 (file)
@@ -62,6 +62,7 @@ descending=Descending
 description=Description
 directories=Directories
 directory=Directory
+dismiss=Dismiss
 display=Display
 download_verb=Download
 duplications=Duplications
@@ -3556,7 +3557,14 @@ maintenance.all_systems_opetational=All systems operational.
 maintenance.is_offline={instance} is offline
 maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Please contact your system administrator.
 
-
+#------------------------------------------------------------------------------
+#
+# INDEXATION
+#
+#------------------------------------------------------------------------------
+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.completed=All project data has been reloaded.
 
 #------------------------------------------------------------------------------
 #