]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11149 Fix loading problems in issues page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 13 Aug 2018 13:19:24 +0000 (15:19 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 16 Aug 2018 07:45:57 +0000 (09:45 +0200)
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx
server/sonar-web/src/main/js/apps/issues/components/ReloadButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.tsx
server/sonar-web/src/main/js/components/controls/BackButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/ListFooter.tsx
server/sonar-web/src/main/js/components/controls/__tests__/ListFooter-test.tsx

index 11bbc4bb9f7aa0e3ae61abc2613009b16c117906..9b7c566d681a768afa7357af1b4acd6832217740 100644 (file)
@@ -308,6 +308,10 @@ td.big-spacer-top {
   align-items: center;
 }
 
+.position-absolute {
+  position: absolute !important;
+}
+
 .rounded {
   border-radius: 2px;
 }
index 330409caa95e789e99ec9843fe37d241348fcda6..1850b50dbda71d907ab47ada8ac17b1f6c32d433 100644 (file)
@@ -19,9 +19,9 @@
  */
 import * as React from 'react';
 import IssuesCounter from './IssuesCounter';
-import ReloadButton from './ReloadButton';
 import { HomePageType, Paging } from '../../../app/types';
 import HomePageSelect from '../../../components/controls/HomePageSelect';
+import ReloadButton from '../../../components/controls/ReloadButton';
 import { translate } from '../../../helpers/l10n';
 import { isSonarCloud } from '../../../helpers/system';
 
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.tsx b/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.tsx
deleted file mode 100644 (file)
index 3af99da..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
-import Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  className?: string;
-  onClick: () => void;
-}
-
-/* eslint-disable max-len */
-const icon = (
-  <svg height="24" viewBox="0 0 18 24" width="18">
-    <path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
-  </svg>
-);
-/* eslint-enable max-len */
-
-export default function ReloadButton(props: Props) {
-  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    props.onClick();
-  };
-
-  return (
-    <Tooltip overlay={translate('reload')}>
-      <a
-        className={classNames('concise-issues-list-header-button', props.className)}
-        href="#"
-        onClick={handleClick}>
-        {icon}
-      </a>
-    </Tooltip>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.js
deleted file mode 100644 (file)
index dfc109d..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// @flow
-import * as React from 'react';
-import { shallow, mount } from 'enzyme';
-import App from '../App';
-import { shallowWithIntl, waitAndUpdate } from '../../../../helpers/testUtils';
-
-const replace = jest.fn();
-const issues = [{ key: 'foo' }, { key: 'bar' }, { key: 'third' }, { key: 'fourth' }];
-const facets = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
-const paging = [{ pageIndex: 1, pageSize: 100, total: 4 }];
-
-const eventNoShiftKey = { shiftKey: false };
-const eventWithShiftKey = { shiftKey: true };
-
-const PROPS = {
-  branch: { isMain: true, name: 'master' },
-  currentUser: {
-    isLoggedIn: true,
-    avatar: 'foo',
-    email: 'forr@bar.com',
-    login: 'JohnDoe',
-    name: 'John Doe'
-  },
-  component: { key: 'foo', name: 'bar', organization: 'John', qualifier: 'Doe' },
-  location: { pathname: '/issues', query: {} },
-  fetchIssues: () => Promise.resolve({ facets, issues, paging }),
-  onBranchesChange: () => {},
-  onSonarCloud: false,
-  organization: { key: 'foo' }
-};
-
-it('should render a list of issue', async () => {
-  const wrapper = shallowWithIntl(<App {...PROPS} />, {
-    context: { router: { replace } }
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().issues.length).toBe(4);
-});
-
-it('should be able to check/uncheck a group of issues with the Shift key', async () => {
-  const wrapper = shallowWithIntl(<App {...PROPS} />, {
-    context: { router: { replace } }
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().issues.length).toBe(4);
-
-  wrapper.instance().handleIssueCheck('foo', eventNoShiftKey);
-  expect(wrapper.state().checked.length).toBe(1);
-
-  wrapper.instance().handleIssueCheck('fourth', eventWithShiftKey);
-  expect(wrapper.state().checked.length).toBe(4);
-
-  wrapper.instance().handleIssueCheck('third', eventNoShiftKey);
-  expect(wrapper.state().checked.length).toBe(3);
-
-  wrapper.instance().handleIssueCheck('foo', eventWithShiftKey);
-  expect(wrapper.state().checked.length).toBe(1);
-});
-
-it('should avoid non-existing keys', async () => {
-  const wrapper = shallowWithIntl(<App {...PROPS} />, {
-    context: { router: { replace } }
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().issues.length).toBe(4);
-
-  wrapper.instance().handleIssueCheck('foo', eventNoShiftKey);
-  expect(wrapper.state().checked.length).toBe(1);
-
-  wrapper.instance().handleIssueCheck('non-existing-key', eventWithShiftKey);
-  expect(wrapper.state().checked.length).toBe(1);
-});
-
-it('should be able to uncheck all issue with global checkbox', async () => {
-  const wrapper = shallowWithIntl(<App {...PROPS} />, {
-    context: { router: { replace } }
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().issues.length).toBe(4);
-
-  wrapper.instance().handleIssueCheck('foo', eventNoShiftKey);
-  wrapper.instance().handleIssueCheck('bar', eventNoShiftKey);
-  expect(wrapper.state().checked.length).toBe(2);
-
-  wrapper.instance().onCheckAll(false);
-  expect(wrapper.state().checked.length).toBe(0);
-});
-
-it('should be able to check all issue with global checkbox', async () => {
-  const wrapper = shallowWithIntl(<App {...PROPS} />, {
-    context: { router: { replace } }
-  });
-
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.state().checked.length).toBe(0);
-  wrapper.instance().onCheckAll(true);
-  expect(wrapper.state().checked.length).toBe(wrapper.state().issues.length);
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..bd16df2
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 App from '../App';
+import { shallowWithIntl, waitAndUpdate } from '../../../../helpers/testUtils';
+import { Issue } from '../../../../app/types';
+
+const replace = jest.fn();
+const issues = [
+  { key: 'foo' } as Issue,
+  { key: 'bar' } as Issue,
+  { key: 'third' } as Issue,
+  { key: 'fourth' } as Issue
+];
+const facets = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
+const paging = { pageIndex: 1, pageSize: 100, total: 4 };
+
+const eventNoShiftKey = { shiftKey: false } as MouseEvent;
+const eventWithShiftKey = { shiftKey: true } as MouseEvent;
+
+const PROPS = {
+  branch: { isMain: true, name: 'master' },
+  currentUser: {
+    isLoggedIn: true,
+    avatar: 'foo',
+    email: 'forr@bar.com',
+    login: 'JohnDoe',
+    name: 'John Doe'
+  },
+  component: { breadcrumbs: [], key: 'foo', name: 'bar', organization: 'John', qualifier: 'Doe' },
+  location: { pathname: '/issues', query: {} },
+  fetchIssues: () =>
+    Promise.resolve({
+      components: [],
+      facets,
+      issues,
+      languages: [],
+      paging,
+      rules: [],
+      users: []
+    }),
+  onBranchesChange: () => {},
+  onSonarCloud: false,
+  organization: { key: 'foo' },
+  userOrganizations: []
+};
+
+it('should render a list of issue', async () => {
+  const wrapper = shallowWithIntl(<App {...PROPS} />, {
+    context: { router: { replace } }
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().issues.length).toBe(4);
+});
+
+it('should be able to check/uncheck a group of issues with the Shift key', async () => {
+  const wrapper = shallowWithIntl(<App {...PROPS} />, {
+    context: { router: { replace } }
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().issues.length).toBe(4);
+
+  const instance = wrapper.instance() as App;
+  instance.handleIssueCheck('foo', eventNoShiftKey);
+  expect(wrapper.state().checked.length).toBe(1);
+
+  instance.handleIssueCheck('fourth', eventWithShiftKey);
+  expect(wrapper.state().checked.length).toBe(4);
+
+  instance.handleIssueCheck('third', eventNoShiftKey);
+  expect(wrapper.state().checked.length).toBe(3);
+
+  instance.handleIssueCheck('foo', eventWithShiftKey);
+  expect(wrapper.state().checked.length).toBe(1);
+});
+
+it('should avoid non-existing keys', async () => {
+  const wrapper = shallowWithIntl(<App {...PROPS} />, {
+    context: { router: { replace } }
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().issues.length).toBe(4);
+
+  const instance = wrapper.instance() as App;
+  instance.handleIssueCheck('foo', eventNoShiftKey);
+  expect(wrapper.state().checked.length).toBe(1);
+
+  instance.handleIssueCheck('non-existing-key', eventWithShiftKey);
+  expect(wrapper.state().checked.length).toBe(1);
+});
+
+it('should be able to uncheck all issue with global checkbox', async () => {
+  const wrapper = shallowWithIntl(<App {...PROPS} />, {
+    context: { router: { replace } }
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().issues.length).toBe(4);
+
+  const instance = wrapper.instance() as App;
+  instance.handleIssueCheck('foo', eventNoShiftKey);
+  instance.handleIssueCheck('bar', eventNoShiftKey);
+  expect(wrapper.state().checked.length).toBe(2);
+
+  instance.onCheckAll(false);
+  expect(wrapper.state().checked.length).toBe(0);
+});
+
+it('should be able to check all issue with global checkbox', async () => {
+  const wrapper = shallowWithIntl(<App {...PROPS} />, {
+    context: { router: { replace } }
+  });
+
+  await waitAndUpdate(wrapper);
+
+  const instance = wrapper.instance() as App;
+  expect(wrapper.state().checked.length).toBe(0);
+  instance.onCheckAll(true);
+  expect(wrapper.state().checked.length).toBe(wrapper.state().issues.length);
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.tsx
deleted file mode 100644 (file)
index d2a9637..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
-import Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  className?: string;
-  onClick: () => void;
-}
-
-/* eslint-disable max-len */
-const icon = (
-  <svg height="24" viewBox="0 0 21 24" width="21">
-    <path d="M3.845 12.9992l5.993 5.993.052.056c.049.061.093.122.129.191.082.159.121.339.111.518-.006.102-.028.203-.064.298-.149.39-.537.652-.954.644-.102-.002-.204-.019-.301-.052-.148-.05-.273-.135-.387-.241l-8.407-8.407 8.407-8.407.056-.052c.061-.048.121-.092.19-.128.116-.06.237-.091.366-.108.076-.004.075-.004.153-.003.155.015.3.052.437.129.088.051.169.115.239.19.246.266.33.656.214.999-.051.149-.135.273-.241.387l-5.983 5.984c5.287-.044 10.577-.206 15.859.013.073.009.091.009.163.027.187.047.359.15.49.292.075.081.136.175.18.276.044.101.072.209.081.319.032.391-.175.775-.521.962-.097.052-.202.089-.311.107-.073.012-.091.01-.165.013H3.845z" />
-  </svg>
-);
-/* eslint-enable max-len */
-
-export default function BackButton(props: Props) {
-  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    props.onClick();
-  };
-
-  return (
-    <Tooltip overlay={translate('issues.return_to_list')}>
-      <a
-        className={classNames('concise-issues-list-header-button', props.className)}
-        href="#"
-        onClick={handleClick}>
-        {icon}
-      </a>
-    </Tooltip>
-  );
-}
index fb50f4f0fecf0a66e7f914306edb385a5299bc95..ffffdeb0a559ded17fd57f1e544c4ee502bbc6a6 100644 (file)
@@ -18,8 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import BackButton from './BackButton';
-import ReloadButton from '../components/ReloadButton';
+import BackButton from '../../../components/controls/BackButton';
+import ReloadButton from '../../../components/controls/ReloadButton';
 import IssuesCounter from '../components/IssuesCounter';
 import { Paging } from '../../../app/types';
 
@@ -38,7 +38,9 @@ export default function ConciseIssuesListHeader(props: Props) {
   return (
     <header className="layout-page-header-panel concise-issues-list-header">
       <div className="layout-page-header-panel-inner concise-issues-list-header-inner">
-        {displayBackButton && <BackButton className="pull-left" onClick={props.onBackClick} />}
+        {displayBackButton && (
+          <BackButton className="pull-left" disabled={props.loading} onClick={props.onBackClick} />
+        )}
         {props.loading ? (
           <i className="spinner pull-right" />
         ) : (
diff --git a/server/sonar-web/src/main/js/components/controls/BackButton.tsx b/server/sonar-web/src/main/js/components/controls/BackButton.tsx
new file mode 100644 (file)
index 0000000..d8d24cc
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import Tooltip from './Tooltip';
+import * as theme from '../../app/theme';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  className?: string;
+  disabled?: boolean;
+  tooltip?: string;
+  onClick: () => void;
+}
+
+export default class BackButton extends React.PureComponent<Props> {
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (!this.props.disabled) {
+      this.props.onClick();
+    }
+  };
+
+  renderIcon = () => (
+    <svg height="24" viewBox="0 0 21 24" width="21">
+      <path
+        d="M3.845 12.9992l5.993 5.993.052.056c.049.061.093.122.129.191.082.159.121.339.111.518-.006.102-.028.203-.064.298-.149.39-.537.652-.954.644-.102-.002-.204-.019-.301-.052-.148-.05-.273-.135-.387-.241l-8.407-8.407 8.407-8.407.056-.052c.061-.048.121-.092.19-.128.116-.06.237-.091.366-.108.076-.004.075-.004.153-.003.155.015.3.052.437.129.088.051.169.115.239.19.246.266.33.656.214.999-.051.149-.135.273-.241.387l-5.983 5.984c5.287-.044 10.577-.206 15.859.013.073.009.091.009.163.027.187.047.359.15.49.292.075.081.136.175.18.276.044.101.072.209.081.319.032.391-.175.775-.521.962-.097.052-.202.089-.311.107-.073.012-.091.01-.165.013H3.845z"
+        fill={this.props.disabled ? theme.disableGrayText : theme.secondFontColor}
+      />
+    </svg>
+  );
+
+  render() {
+    const { tooltip = translate('issues.return_to_list') } = this.props;
+    return (
+      <Tooltip overlay={tooltip}>
+        <a
+          className={classNames(
+            'link-no-underline',
+            { 'cursor-not-allowed': this.props.disabled },
+            this.props.className
+          )}
+          href="#"
+          onClick={this.handleClick}>
+          {this.renderIcon()}
+        </a>
+      </Tooltip>
+    );
+  }
+}
index 29113a8edbb7f7d0a1f7728af6d715365d0ff1b0..9ac22f64c89253ab9033df0740cc39e2f891a98a 100644 (file)
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
+import DeferredSpinner from '../common/DeferredSpinner';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { formatMeasure } from '../../helpers/measures';
 
 interface Props {
   count: number;
   className?: string;
+  loading?: boolean;
   loadMore?: () => void;
   ready?: boolean;
   total?: number;
@@ -59,6 +61,9 @@ export default function ListFooter({ ready = true, ...props }: Props) {
         formatMeasure(props.total, 'INT', null)
       )}
       {props.loadMore != null && hasMore ? loadMoreLink : null}
+      {props.loading && (
+        <DeferredSpinner className="vertical-bottom spacer-left position-absolute" />
+      )}
     </footer>
   );
 }
index b8de25e41f193603081d5bb887746be74baadda5..dab8c4d33abae85a408376ffc0d23941e98d6d75 100644 (file)
@@ -45,3 +45,11 @@ it('should "show more"', () => {
   click(link);
   expect(loadMore).toBeCalled();
 });
+
+it('should display spinner while loading', () => {
+  expect(
+    shallow(<ListFooter count={3} loadMore={jest.fn()} loading={true} total={10} />)
+      .find('DeferredSpinner')
+      .exists()
+  ).toBe(true);
+});