]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14938 Fallback on the Create Project page on brand new instance
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 17 Jun 2021 15:15:11 +0000 (17:15 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 22 Jun 2021 20:03:13 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/routes.ts

index 8668c78e9c51af486bbf9f53ca5b64c0b6cf0de8..56ef87faafb5da6c116f300cd068305fe538b7e9 100644 (file)
@@ -20,8 +20,9 @@
 import * as React from 'react';
 import { get } from 'sonar-ui-common/helpers/storage';
 import { searchProjects } from '../../../api/components';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { isLoggedIn } from '../../../helpers/users';
+import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
 import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils';
 import AllProjectsContainer from './AllProjectsContainer';
 
@@ -32,84 +33,68 @@ interface Props {
 }
 
 interface State {
-  shouldBeRedirected?: boolean;
-  shouldForceSorting?: string;
+  checking: boolean;
 }
 
 export class DefaultPageSelector extends React.PureComponent<Props, State> {
-  state: State = {};
+  state: State = { checking: true };
 
   componentDidMount() {
-    this.defineIfShouldBeRedirected();
+    this.checkIfNeedsRedirecting();
   }
 
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.location !== this.props.location) {
-      this.defineIfShouldBeRedirected();
-    } else if (this.state.shouldBeRedirected === true) {
-      this.props.router.replace({ ...this.props.location, pathname: '/projects/favorite' });
-    } else if (this.state.shouldForceSorting != null) {
-      this.props.router.replace({
-        ...this.props.location,
-        query: {
-          ...this.props.location.query,
-          sort: this.state.shouldForceSorting
-        }
-      });
-    }
-  }
-
-  isFavoriteSet = (): boolean => {
-    const setting = get(PROJECTS_DEFAULT_FILTER);
-    return setting === PROJECTS_FAVORITE;
-  };
-
-  isAllSet = (): boolean => {
+  checkIfNeedsRedirecting = async () => {
+    const { currentUser, router, location } = this.props;
     const setting = get(PROJECTS_DEFAULT_FILTER);
-    return setting === PROJECTS_ALL;
-  };
 
-  defineIfShouldBeRedirected() {
-    if (Object.keys(this.props.location.query).length > 0) {
-      // show ALL projects when there are some filters
-      this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
-    } else if (!isLoggedIn(this.props.currentUser)) {
-      // show ALL projects if user is anonymous
-      if (!this.props.location.query || !this.props.location.query.sort) {
-        // force default sorting to last analysis date
-        this.setState({ shouldBeRedirected: false, shouldForceSorting: '-analysis_date' });
-      } else {
-        this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
-      }
-    } else if (this.isFavoriteSet()) {
-      // show FAVORITE projects if "favorite" setting is explicitly set
-      this.setState({ shouldBeRedirected: true, shouldForceSorting: undefined });
-    } else if (this.isAllSet()) {
-      // show ALL projects if "all" setting is explicitly set
-      this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
-    } else {
-      // otherwise, request favorites
-      this.setState({ shouldBeRedirected: undefined, shouldForceSorting: undefined });
-      searchProjects({ filter: 'isFavorite', ps: 1 }).then(r => {
-        // show FAVORITE projects if there are any
-        this.setState({ shouldBeRedirected: r.paging.total > 0, shouldForceSorting: undefined });
-      });
+    // 1. Don't have to redirect if:
+    //   1.1 User is anonymous
+    //   1.2 There's a query, which means the user is interacting with the current page
+    //   1.3 The last interaction with the filter was to set it to "all"
+    if (
+      !isLoggedIn(currentUser) ||
+      Object.keys(location.query).length > 0 ||
+      setting === PROJECTS_ALL
+    ) {
+      this.setState({ checking: false });
+      return;
     }
-  }
 
-  render() {
-    const { shouldBeRedirected, shouldForceSorting } = this.state;
+    // 2. Redirect to the favorites page if:
+    //   2.1 The last interaction with the filter was to set it to "favorites"
+    //   2.2 The user has starred some projects
+    if (
+      setting === PROJECTS_FAVORITE ||
+      (await searchProjects({ filter: 'isFavorite', ps: 1 })).paging.total > 0
+    ) {
+      router.replace('/projects/favorite');
+      return;
+    }
 
+    // 3. Redirect to the create project page if:
+    //   3.1 The user has permission to provision projects, AND there are 0 projects on the instance
     if (
-      shouldBeRedirected !== undefined &&
-      shouldBeRedirected !== true &&
-      shouldForceSorting === undefined
+      hasGlobalPermission(currentUser, 'provisioning') &&
+      (await searchProjects({ ps: 1 })).paging.total === 0
     ) {
-      return <AllProjectsContainer isFavorite={false} />;
+      this.props.router.replace('/projects/create');
+    }
+
+    // None of the above apply. Do not redirect, and stay on this page.
+    this.setState({ checking: false });
+  };
+
+  render() {
+    const { checking } = this.state;
+
+    if (checking) {
+      // We don't return a loader here, on purpose. We don't want to show anything
+      // just yet.
+      return null;
     }
 
-    return null;
+    return <AllProjectsContainer isFavorite={false} />;
   }
 }
 
-export default withRouter(DefaultPageSelector);
+export default withCurrentUser(withRouter(DefaultPageSelector));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx
deleted file mode 100644 (file)
index 6f69359..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { connect } from 'react-redux';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import DefaultPageSelector from './DefaultPageSelector';
-
-const stateToProps = (state: Store) => ({
-  currentUser: getCurrentUser(state)
-});
-
-export default connect(stateToProps)(DefaultPageSelector);
index 9cb1171eaaaf3109a7f0683c0978a5a2d25d339a..1445bf40746b6480e6b00e98da7a77aab0ec7279 100644 (file)
  * 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 { shallow } from 'enzyme';
 import * as React from 'react';
 import { get } from 'sonar-ui-common/helpers/storage';
-import { doAsync } from 'sonar-ui-common/helpers/testUtils';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { searchProjects } from '../../../../api/components';
+import {
+  mockCurrentUser,
+  mockLocation,
+  mockLoggedInUser,
+  mockRouter
+} from '../../../../helpers/testMocks';
+import { hasGlobalPermission } from '../../../../helpers/users';
 import { DefaultPageSelector } from '../DefaultPageSelector';
 
 jest.mock('../AllProjectsContainer', () => ({
@@ -32,63 +39,116 @@ jest.mock('../AllProjectsContainer', () => ({
 }));
 
 jest.mock('sonar-ui-common/helpers/storage', () => ({
-  get: jest.fn()
+  get: jest.fn().mockReturnValue(undefined)
+}));
+
+jest.mock('../../../../helpers/users', () => ({
+  hasGlobalPermission: jest.fn().mockReturnValue(false),
+  isLoggedIn: jest.fn((u: T.CurrentUser) => u.isLoggedIn)
 }));
 
 jest.mock('../../../../api/components', () => ({
-  searchProjects: jest.fn()
+  searchProjects: jest.fn().mockResolvedValue({ paging: { total: 0 } })
 }));
 
-beforeEach(() => {
-  (get as jest.Mock).mockImplementation(() => '').mockClear();
+beforeEach(jest.clearAllMocks);
+
+it('renders correctly', () => {
+  expect(shallowRender({ currentUser: mockLoggedInUser() }).type()).toBeNull(); // checking
+  expect(shallowRender({ currentUser: mockCurrentUser() })).toMatchSnapshot('default');
+});
+
+it("1.1 doesn't redirect for anonymous users", async () => {
+  const replace = jest.fn();
+  const wrapper = shallowRender({
+    currentUser: mockCurrentUser(),
+    router: mockRouter({ replace })
+  });
+  await waitAndUpdate(wrapper);
+  expect(replace).not.toBeCalled();
+});
+
+it("1.2 doesn't redirect if there's an existing filter in location", async () => {
+  const replace = jest.fn();
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { size: '1' } }),
+    router: mockRouter({ replace })
+  });
+
+  await waitAndUpdate(wrapper);
+
+  expect(replace).not.toBeCalled();
 });
 
-it('shows all projects with existing filter', () => {
+it("1.3 doesn't redirect if the user previously used the 'all' filter", async () => {
+  (get as jest.Mock).mockReturnValueOnce('all');
   const replace = jest.fn();
-  mountRender(undefined, { size: '1' }, replace);
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
   expect(replace).not.toBeCalled();
 });
 
-it('shows all projects sorted by analysis date for anonymous', () => {
+it('2.1 redirects to favorites if the user previously used the "favorites" filter', async () => {
+  (get as jest.Mock).mockReturnValueOnce('favorite');
+  const replace = jest.fn();
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
+  expect(replace).toBeCalledWith('/projects/favorite');
+});
+
+it('2.2 redirects to favorites if the user has starred projects', async () => {
+  (searchProjects as jest.Mock).mockResolvedValueOnce({ paging: { total: 3 } });
   const replace = jest.fn();
-  mountRender({ isLoggedIn: false }, undefined, replace);
-  expect(replace).lastCalledWith({ pathname: '/projects', query: { sort: '-analysis_date' } });
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
+  expect(searchProjects).toHaveBeenLastCalledWith({ filter: 'isFavorite', ps: 1 });
+  expect(replace).toBeCalledWith('/projects/favorite');
 });
 
-it('shows favorite projects', () => {
-  (get as jest.Mock).mockImplementation(() => 'favorite');
+it('3.1 redirects to create project page, if user has correct permissions AND there are 0 projects', async () => {
+  (hasGlobalPermission as jest.Mock).mockReturnValueOnce(true);
   const replace = jest.fn();
-  mountRender(undefined, undefined, replace);
-  expect(replace).lastCalledWith({ pathname: '/projects/favorite', query: {} });
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
+  expect(replace).toBeCalledWith('/projects/create');
 });
 
-it('shows all projects', () => {
-  (get as jest.Mock).mockImplementation(() => 'all');
+it("3.1 doesn't redirect to create project page, if user has no permissions", async () => {
   const replace = jest.fn();
-  mountRender(undefined, undefined, replace);
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
   expect(replace).not.toBeCalled();
 });
 
-it('fetches favorites', () => {
-  (searchProjects as jest.Mock).mockImplementation(() => Promise.resolve({ paging: { total: 3 } }));
+it("3.1 doesn't redirect to create project page, if there's existing projects", async () => {
+  (searchProjects as jest.Mock)
+    .mockResolvedValueOnce({ paging: { total: 0 } }) // no favorites
+    .mockResolvedValueOnce({ paging: { total: 3 } }); // existing projects
   const replace = jest.fn();
-  mountRender(undefined, undefined, replace);
-  return doAsync().then(() => {
-    expect(searchProjects).toHaveBeenLastCalledWith({ filter: 'isFavorite', ps: 1 });
-    expect(replace).toBeCalledWith({ pathname: '/projects/favorite', query: {} });
-  });
+  const wrapper = shallowRender({ router: mockRouter({ replace }) });
+
+  await waitAndUpdate(wrapper);
+
+  expect(replace).not.toBeCalled();
 });
 
-function mountRender(
-  currentUser: T.CurrentUser = { isLoggedIn: true },
-  query: any = {},
-  replace: any = jest.fn()
-) {
-  return mount(
+function shallowRender(props: Partial<DefaultPageSelector['props']> = {}) {
+  return shallow<DefaultPageSelector>(
     <DefaultPageSelector
-      currentUser={currentUser}
-      location={{ pathname: '/projects', query }}
-      router={{ replace }}
+      currentUser={mockLoggedInUser()}
+      location={mockLocation({ pathname: '/projects' })}
+      router={mockRouter()}
+      {...props}
     />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/DefaultPageSelector-test.tsx.snap
new file mode 100644 (file)
index 0000000..92729bd
--- /dev/null
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly: default 1`] = `
+<AllProjectsContainer
+  isFavorite={false}
+/>
+`;
index 609943818acbbc557ba98c46ebc681831b1ac160..e1d22ef0dafa34bbc7e82ad8191f338c81243a83 100644 (file)
 import { RedirectFunction, RouterState } from 'react-router';
 import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
 import { save } from 'sonar-ui-common/helpers/storage';
-import { isDefined } from 'sonar-ui-common/helpers/types';
-import DefaultPageSelectorContainer from './components/DefaultPageSelectorContainer';
-import FavoriteProjectsContainer from './components/FavoriteProjectsContainer';
 import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER } from './utils';
 
 const routes = [
-  { indexRoute: { component: DefaultPageSelectorContainer } },
+  {
+    indexRoute: { component: lazyLoadComponent(() => import('./components/DefaultPageSelector')) }
+  },
   {
     path: 'all',
     onEnter(_: RouterState, replace: RedirectFunction) {
@@ -34,11 +33,14 @@ const routes = [
       replace('/projects');
     }
   },
-  { path: 'favorite', component: FavoriteProjectsContainer },
+  {
+    path: 'favorite',
+    component: lazyLoadComponent(() => import('./components/FavoriteProjectsContainer'))
+  },
   {
     path: 'create',
     component: lazyLoadComponent(() => import('../create/project/CreateProjectPage'))
   }
-].filter(isDefined);
+];
 
 export default routes;