diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index f837845ab636ca33091e88e8823c281f6bf4c616..3dcfe1c8bb95363bbcc0ba6a349f5900f1de3364 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -13,17 +13,19 @@ import {
   FILTERED_SEARCH_TERM,
   TOKEN_EMPTY_SEARCH_TERM,
 } from '~/vue_shared/components/filtered_search_bar/constants';
-import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
-import GroupsView from '../../shared/components/groups_view.vue';
-import ProjectsView from '../../shared/components/projects_view.vue';
-import { onPageChange } from '../../shared/utils';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
+import GroupsView from '~/organizations/shared/components/groups_view.vue';
+import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
+import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
+import { onPageChange } from '~/organizations/shared/utils';
 import {
   QUERY_PARAM_END_CURSOR,
   QUERY_PARAM_START_CURSOR,
   SORT_DIRECTION_ASC,
   SORT_DIRECTION_DESC,
   SORT_ITEM_NAME,
-} from '../../shared/constants';
+} from '~/organizations/shared/constants';
 import { DISPLAY_LISTBOX_ITEMS, SORT_ITEMS, FILTERED_SEARCH_TERM_KEY } from '../constants';
 
 export default {
@@ -32,7 +34,13 @@ export default {
     searchInputPlaceholder: s__('Organization|Search or filter list'),
     displayListboxHeaderText: __('Display'),
   },
-  components: { FilteredSearchBar, GlCollapsibleListbox, GlSorting },
+  components: {
+    FilteredSearchBar,
+    GlCollapsibleListbox,
+    GlSorting,
+    NewGroupButton,
+    NewProjectButton,
+  },
   filteredSearch: {
     tokens: [],
     namespace: 'organization_groups_and_projects',
@@ -156,7 +164,15 @@ export default {
 
 <template>
   <div>
-    <h1 class="gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
+    <div
+      class="page-title-holder gl-display-flex gl-sm-flex-direction-row gl-flex-direction-column gl-sm-align-items-center"
+    >
+      <h1 class="page-title gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
+      <div class="gl-display-flex gl-column-gap-3 gl-sm-ml-auto gl-mb-4 gl-sm-mb-0">
+        <new-group-button category="secondary" />
+        <new-project-button />
+      </div>
+    </div>
     <div class="gl-p-5 gl-bg-gray-10 gl-border-t gl-border-b">
       <div class="gl-mx-n2 gl-my-n2 gl-md-display-flex">
         <div class="gl-p-2 gl-flex-grow-1">
diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js
index 8f6ae7f04cb7ef373a77c889e1526f5096967089..ead747c341539789c214e1309df0724619058426 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/index.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/index.js
@@ -32,6 +32,9 @@ export const initOrganizationsGroupsAndProjects = () => {
     groupsEmptyStateSvgPath,
     newGroupPath,
     newProjectPath,
+    canCreateGroup,
+    canCreateProject,
+    hasGroups,
   } = convertObjectPropsToCamelCase(JSON.parse(appData));
 
   Vue.use(VueRouter);
@@ -51,6 +54,9 @@ export const initOrganizationsGroupsAndProjects = () => {
       groupsEmptyStateSvgPath,
       newGroupPath,
       newProjectPath,
+      canCreateGroup,
+      canCreateProject,
+      hasGroups,
     },
     render(createElement) {
       return createElement(App);
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
index 11dd8b10193d0bcffd3e1c0a2d7115840b43e019..9822c7ee56b2fd65e3ab910fc07570f8eb8c9a35 100644
--- a/app/assets/javascripts/organizations/shared/components/groups_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -7,6 +7,7 @@ import { DEFAULT_PER_PAGE } from '~/api';
 import groupsQuery from '../graphql/queries/groups.query.graphql';
 import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
 import { formatGroups } from '../utils';
+import NewGroupButton from './new_group_button.vue';
 
 export default {
   i18n: {
@@ -18,19 +19,14 @@ export default {
       description: s__(
         'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
       ),
-      primaryButtonText: __('New group'),
     },
-
     prev: __('Prev'),
     next: __('Next'),
   },
-  components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList },
+  components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList, NewGroupButton },
   inject: {
     organizationGid: {},
     groupsEmptyStateSvgPath: {},
-    newGroupPath: {
-      default: null,
-    },
   },
   props: {
     shouldShowEmptyStateButtons: {
@@ -143,14 +139,6 @@ export default {
         description: this.$options.i18n.emptyState.description,
       };
 
-      if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
-        return {
-          ...baseProps,
-          primaryButtonLink: this.newGroupPath,
-          primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
-        };
-      }
-
       return baseProps;
     },
   },
@@ -186,5 +174,9 @@ export default {
       />
     </div>
   </div>
-  <gl-empty-state v-else v-bind="emptyStateProps" />
+  <gl-empty-state v-else v-bind="emptyStateProps">
+    <template v-if="shouldShowEmptyStateButtons" #actions>
+      <new-group-button />
+    </template>
+  </gl-empty-state>
 </template>
diff --git a/app/assets/javascripts/organizations/shared/components/new_group_button.vue b/app/assets/javascripts/organizations/shared/components/new_group_button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8254735192a5b9ed1aff6b2d426e0b8e64ce4b52
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/new_group_button.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+  i18n: {
+    newGroup: __('New group'),
+  },
+  components: {
+    GlButton,
+  },
+  inject: ['canCreateGroup', 'newGroupPath'],
+  props: {
+    category: {
+      type: String,
+      required: false,
+      default: 'primary',
+    },
+  },
+  computed: {
+    showButton() {
+      return this.canCreateGroup && this.newGroupPath;
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-button v-if="showButton" :href="newGroupPath" :category="category" variant="confirm">{{
+    $options.i18n.newGroup
+  }}</gl-button>
+</template>
diff --git a/app/assets/javascripts/organizations/shared/components/new_project_button.vue b/app/assets/javascripts/organizations/shared/components/new_project_button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c7d39e8e31fac2207a748b1603d4c4870b763293
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/new_project_button.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+
+export default {
+  i18n: {
+    newProjectButtonDisabledTooltip: s__(
+      'Organization|Projects are hosted/created in groups. Before creating a project, you must create a group.',
+    ),
+    newProject: __('New project'),
+  },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
+  components: {
+    GlButton,
+  },
+  inject: ['hasGroups', 'canCreateProject', 'newProjectPath'],
+  computed: {
+    showButton() {
+      return this.canCreateProject && this.newProjectPath;
+    },
+    tooltip() {
+      return this.hasGroups ? null : this.$options.i18n.newProjectButtonDisabledTooltip;
+    },
+  },
+};
+</script>
+
+<template>
+  <span
+    v-if="showButton"
+    v-gl-tooltip
+    :title="tooltip"
+    data-testid="new-project-button-tooltip-container"
+    ><gl-button
+      :href="newProjectPath"
+      :disabled="!hasGroups"
+      category="primary"
+      variant="confirm"
+      >{{ $options.i18n.newProject }}</gl-button
+    ></span
+  >
+</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
index 20faa6e82d53e9c5275dedccee5cd974194f0133..f6d0d70e24ed18276e586d1fb2a6855c727d97ee 100644
--- a/app/assets/javascripts/organizations/shared/components/projects_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -7,6 +7,7 @@ import { createAlert } from '~/alert';
 import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
 import projectsQuery from '../graphql/queries/projects.query.graphql';
 import { formatProjects } from '../utils';
+import NewProjectButton from './new_project_button.vue';
 
 export default {
   i18n: {
@@ -18,7 +19,6 @@ export default {
       description: s__(
         'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
       ),
-      primaryButtonText: __('New project'),
     },
     prev: __('Prev'),
     next: __('Next'),
@@ -28,13 +28,11 @@ export default {
     GlLoadingIcon,
     GlEmptyState,
     GlKeysetPagination,
+    NewProjectButton,
   },
   inject: {
     organizationGid: {},
     projectsEmptyStateSvgPath: {},
-    newProjectPath: {
-      default: null,
-    },
   },
   props: {
     shouldShowEmptyStateButtons: {
@@ -149,14 +147,6 @@ export default {
         description: this.$options.i18n.emptyState.description,
       };
 
-      if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
-        return {
-          ...baseProps,
-          primaryButtonLink: this.newProjectPath,
-          primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
-        };
-      }
-
       return baseProps;
     },
   },
@@ -191,5 +181,9 @@ export default {
       />
     </div>
   </div>
-  <gl-empty-state v-else v-bind="emptyStateProps" />
+  <gl-empty-state v-else v-bind="emptyStateProps">
+    <template v-if="shouldShowEmptyStateButtons" #actions>
+      <new-project-button />
+    </template>
+  </gl-empty-state>
 </template>
diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js
index 0d927eeea8abf8494ca7042a3895f1d531e425ef..940161ef0916bf50992aaf9c77ad91bf7516c62b 100644
--- a/app/assets/javascripts/organizations/show/index.js
+++ b/app/assets/javascripts/organizations/show/index.js
@@ -35,6 +35,9 @@ export const initOrganizationsShow = () => {
     newGroupPath,
     newProjectPath,
     associationCounts,
+    canCreateProject,
+    canCreateGroup,
+    hasGroups,
   } = convertObjectPropsToCamelCase(JSON.parse(appData));
 
   Vue.use(VueRouter);
@@ -54,6 +57,9 @@ export const initOrganizationsShow = () => {
       groupsEmptyStateSvgPath,
       newGroupPath,
       newProjectPath,
+      canCreateProject,
+      canCreateGroup,
+      hasGroups,
     },
     render(createElement) {
       return createElement(App, {
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index e18cd409d73dc71ca177992bc5c85164db589b49..ba386bca2a05e83f4c3e3d6ff4a0ee8c764a1aad 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -77,7 +77,10 @@ def shared_groups_and_projects_app_data(organization)
         projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'),
         groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'),
         new_group_path: new_group_path,
-        new_project_path: new_project_path
+        new_project_path: new_project_path,
+        can_create_group: can?(current_user, :create_group, organization),
+        can_create_project: current_user&.can_create_project?,
+        has_groups: has_groups?(organization)
       }
     end
 
@@ -95,5 +98,9 @@ def organizations_users_paths
         admin_user: admin_user_path(:id)
       }
     end
+
+    def has_groups?(organization)
+      organization.groups.exists?
+    end
   end
 end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 184e342f6638597ab14a0b79a220fdd28d26dc00..a3c82eb0e380d1a78ca082062db4edee54a5ebdc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -34523,6 +34523,9 @@ msgstr ""
 msgid "Organization|Perform advanced options such as deleting the organization."
 msgstr ""
 
+msgid "Organization|Projects are hosted/created in groups. Before creating a project, you must create a group."
+msgstr ""
+
 msgid "Organization|Public - The organization can be accessed without any authentication."
 msgstr ""
 
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
index ebc84c9efb0e3973459eaf8f6ae76d96711adcbc..1f6a153a9e1bc653c442c97c35db34cce209ed66 100644
--- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -2,6 +2,8 @@ import { GlCollapsibleListbox, GlSorting } from '@gitlab/ui';
 import App from '~/organizations/groups_and_projects/components/app.vue';
 import GroupsView from '~/organizations/shared/components/groups_view.vue';
 import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
+import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
 import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
 import { SORT_ITEMS } from '~/organizations/groups_and_projects/constants';
 import {
@@ -35,10 +37,19 @@ describe('GroupsAndProjectsApp', () => {
     });
   };
 
+  const findPageTitle = () => wrapper.findByText('Groups and projects');
   const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
   const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
   const findSort = () => wrapper.findComponent(GlSorting);
   const findProjectsView = () => wrapper.findComponent(ProjectsView);
+  const findNewGroupButton = () => wrapper.findComponent(NewGroupButton);
+  const findNewProjectButton = () => wrapper.findComponent(NewProjectButton);
+
+  it('renders page title as Groups and projects', () => {
+    createComponent();
+
+    expect(findPageTitle().exists()).toBe(true);
+  });
 
   describe.each`
     display                   | expectedComponent | expectedDisplayListboxSelectedProp
@@ -101,6 +112,20 @@ describe('GroupsAndProjectsApp', () => {
     });
   });
 
+  describe('actions', () => {
+    beforeEach(() => {
+      createComponent();
+    });
+
+    it('renders NewProjectButton', () => {
+      expect(findNewProjectButton().exists()).toBe(true);
+    });
+
+    it('renders NewGroupButton with correct props', () => {
+      expect(findNewGroupButton().props()).toStrictEqual({ category: 'secondary' });
+    });
+  });
+
   it('renders sort dropdown with sort items and correct props', () => {
     createComponent();
 
diff --git a/spec/frontend/organizations/shared/components/groups_view_spec.js b/spec/frontend/organizations/shared/components/groups_view_spec.js
index 55d2c36f625c03cb0ffd7440b61db33dcd463d1e..b97dcf59aac4c5eaf455529cc89a1f4fb33b51c6 100644
--- a/spec/frontend/organizations/shared/components/groups_view_spec.js
+++ b/spec/frontend/organizations/shared/components/groups_view_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import { GlEmptyState, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
 import GroupsView from '~/organizations/shared/components/groups_view.vue';
 import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
+import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
 import { formatGroups } from '~/organizations/shared/utils';
 import groupsQuery from '~/organizations/shared/graphql/queries/groups.query.graphql';
 import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
@@ -67,6 +68,7 @@ describe('GroupsView', () => {
   };
 
   const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+  const findNewGroupButton = () => wrapper.findComponent(NewGroupButton);
 
   afterEach(() => {
     mockApollo = null;
@@ -81,51 +83,47 @@ describe('GroupsView', () => {
   });
 
   describe('when API call is successful', () => {
-    describe('when there are no groups', () => {
-      const emptyHandler = jest.fn().mockResolvedValue({
-        data: {
-          organization: {
-            id: defaultProvide.organizationGid,
-            groups: {
-              nodes: [],
-              pageInfo: pageInfoEmpty,
+    describe.each`
+      shouldShowEmptyStateButtons
+      ${false}
+      ${true}
+    `(
+      'when there are no groups and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`',
+      ({ shouldShowEmptyStateButtons }) => {
+        const emptyHandler = jest.fn().mockResolvedValue({
+          data: {
+            organization: {
+              id: defaultProvide.organizationGid,
+              groups: {
+                nodes: [],
+                pageInfo: pageInfoEmpty,
+              },
             },
           },
-        },
-      });
-
-      it('renders empty state without buttons by default', async () => {
-        createComponent({ handler: emptyHandler });
-
-        await waitForPromises();
-
-        expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
-          title: "You don't have any groups yet.",
-          description:
-            'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
-          svgHeight: 144,
-          svgPath: defaultProvide.groupsEmptyStateSvgPath,
-          primaryButtonLink: null,
-          primaryButtonText: null,
         });
-      });
 
-      describe('when `shouldShowEmptyStateButtons` is `true` and `groupsEmptyStateSvgPath` is set', () => {
-        it('renders empty state with buttons', async () => {
+        it(`renders empty state ${
+          shouldShowEmptyStateButtons ? 'with' : 'without'
+        } buttons`, async () => {
           createComponent({
             handler: emptyHandler,
-            propsData: { shouldShowEmptyStateButtons: true },
+            propsData: { shouldShowEmptyStateButtons },
           });
 
           await waitForPromises();
 
           expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
-            primaryButtonLink: defaultProvide.newGroupPath,
-            primaryButtonText: 'New group',
+            title: "You don't have any groups yet.",
+            description:
+              'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
+            svgHeight: 144,
+            svgPath: defaultProvide.groupsEmptyStateSvgPath,
           });
+
+          expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons);
         });
-      });
-    });
+      },
+    );
 
     describe('when there are groups', () => {
       beforeEach(() => {
diff --git a/spec/frontend/organizations/shared/components/new_group_button_spec.js b/spec/frontend/organizations/shared/components/new_group_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ca628478a578ee30bdf419478b3eb830a97a876
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/new_group_button_spec.js
@@ -0,0 +1,80 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
+
+describe('NewGroupButton', () => {
+  let wrapper;
+
+  const defaultProvide = {
+    canCreateGroup: false,
+    newGroupPath: '',
+  };
+
+  const defaultProps = {
+    category: 'primary',
+  };
+
+  function createComponent({ provide = {}, props = {} } = {}) {
+    wrapper = shallowMount(NewGroupButton, {
+      provide: {
+        ...defaultProvide,
+        ...provide,
+      },
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+    });
+  }
+
+  const findGlButton = () => wrapper.findComponent(GlButton);
+
+  describe.each`
+    canCreateGroup | newGroupPath
+    ${false}       | ${null}
+    ${false}       | ${'/asdf'}
+    ${true}        | ${null}
+  `(
+    'when `canCreateGroup` is $canCreateGroup and `newGroupPath` is $newGroupPath',
+    ({ canCreateGroup, newGroupPath }) => {
+      beforeEach(() => {
+        createComponent({ provide: { canCreateGroup, newGroupPath } });
+      });
+
+      it('renders nothing', () => {
+        expect(wrapper.html()).toBe('');
+      });
+    },
+  );
+
+  describe('when `canCreateGroup` is true and `newGroupPath` is /asdf', () => {
+    const newGroupPath = '/asdf';
+
+    describe('with no category', () => {
+      beforeEach(() => {
+        createComponent({
+          provide: { canCreateGroup: true, newGroupPath },
+          props: { category: undefined },
+        });
+      });
+
+      it('renders GlButton correctly', () => {
+        expect(findGlButton().attributes('href')).toBe(newGroupPath);
+        expect(findGlButton().props('category')).toBe(defaultProps.category);
+      });
+    });
+
+    describe('with set category', () => {
+      const category = 'secondary';
+
+      beforeEach(() => {
+        createComponent({ provide: { canCreateGroup: true, newGroupPath }, props: { category } });
+      });
+
+      it('renders GlButton correctly', () => {
+        expect(findGlButton().attributes('href')).toBe(newGroupPath);
+        expect(findGlButton().props('category')).toBe(category);
+      });
+    });
+  });
+});
diff --git a/spec/frontend/organizations/shared/components/new_project_button_spec.js b/spec/frontend/organizations/shared/components/new_project_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..31f6933d0c73c7a184ba9ac09963c6321fdc6137
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/new_project_button_spec.js
@@ -0,0 +1,77 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
+
+describe('NewProjectButton', () => {
+  let wrapper;
+
+  const defaultProvide = {
+    canCreateProject: false,
+    newProjectPath: '',
+    hasGroups: false,
+  };
+
+  function createComponent({ provide = {} } = {}) {
+    wrapper = shallowMountExtended(NewProjectButton, {
+      provide: {
+        ...defaultProvide,
+        ...provide,
+      },
+    });
+  }
+
+  const findTooltipContainer = () => wrapper.findByTestId('new-project-button-tooltip-container');
+  const findGlButton = () => wrapper.findComponent(GlButton);
+
+  describe.each`
+    canCreateProject | newProjectPath
+    ${false}         | ${null}
+    ${false}         | ${'/asdf'}
+    ${true}          | ${null}
+  `(
+    'when `canCreateProject` is $canCreateProject and `newProjectPath` is $newProjectPath',
+    ({ canCreateProject, newProjectPath }) => {
+      beforeEach(() => {
+        createComponent({ provide: { canCreateProject, newProjectPath } });
+      });
+
+      it('renders nothing', () => {
+        expect(wrapper.html()).toBe('');
+      });
+    },
+  );
+
+  describe('when `canCreateProject` is true and `newProjectPath` is /asdf', () => {
+    const newProjectPath = '/asdf';
+
+    beforeEach(() => {
+      createComponent({ provide: { canCreateProject: true, newProjectPath } });
+    });
+
+    it('renders GlButton correctly', () => {
+      expect(findGlButton().attributes('href')).toBe(newProjectPath);
+    });
+  });
+
+  describe.each`
+    hasGroups | disabled     | tooltip
+    ${false}  | ${'true'}    | ${'Projects are hosted/created in groups. Before creating a project, you must create a group.'}
+    ${true}   | ${undefined} | ${undefined}
+  `(
+    'when `canCreateProject` is true , `newProjectPath` is /asdf, and hasGroups is $hasGroups',
+    ({ hasGroups, disabled, tooltip }) => {
+      beforeEach(() => {
+        createComponent({
+          provide: { canCreateProject: true, newProjectPath: '/asdf', hasGroups },
+        });
+      });
+
+      it(`renders GlButton as ${disabled ? 'disabled' : 'not disabled'} with ${
+        tooltip ? 'tooltip' : 'no tooltip'
+      }`, () => {
+        expect(findGlButton().attributes('disabled')).toBe(disabled);
+        expect(findTooltipContainer().attributes('title')).toBe(tooltip);
+      });
+    },
+  );
+});
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js
index c605b777afce911a16ffd9013a699cddab59751e..83376a34df6add22cc082866bb2838c2e5efa75b 100644
--- a/spec/frontend/organizations/shared/components/projects_view_spec.js
+++ b/spec/frontend/organizations/shared/components/projects_view_spec.js
@@ -2,6 +2,7 @@ import VueApollo from 'vue-apollo';
 import Vue from 'vue';
 import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
 import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
 import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
 import { formatProjects } from '~/organizations/shared/utils';
 import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
@@ -66,6 +67,7 @@ describe('ProjectsView', () => {
   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
   const findEmptyState = () => wrapper.findComponent(GlEmptyState);
   const findProjectsList = () => wrapper.findComponent(ProjectsList);
+  const findNewProjectButton = () => wrapper.findComponent(NewProjectButton);
 
   afterEach(() => {
     mockApollo = null;
@@ -80,51 +82,47 @@ describe('ProjectsView', () => {
   });
 
   describe('when API call is successful', () => {
-    describe('when there are no projects', () => {
-      const emptyHandler = jest.fn().mockResolvedValue({
-        data: {
-          organization: {
-            id: defaultProvide.organizationGid,
-            projects: {
-              nodes: [],
-              pageInfo: pageInfoEmpty,
+    describe.each`
+      shouldShowEmptyStateButtons
+      ${false}
+      ${true}
+    `(
+      'when there are no projects and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`',
+      ({ shouldShowEmptyStateButtons }) => {
+        const emptyHandler = jest.fn().mockResolvedValue({
+          data: {
+            organization: {
+              id: defaultProvide.organizationGid,
+              projects: {
+                nodes: [],
+                pageInfo: pageInfoEmpty,
+              },
             },
           },
-        },
-      });
-
-      it('renders empty state without buttons by default', async () => {
-        createComponent({ handler: emptyHandler });
-
-        await waitForPromises();
-
-        expect(findEmptyState().props()).toMatchObject({
-          title: "You don't have any projects yet.",
-          description:
-            'Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
-          svgHeight: 144,
-          svgPath: defaultProvide.projectsEmptyStateSvgPath,
-          primaryButtonLink: null,
-          primaryButtonText: null,
         });
-      });
 
-      describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => {
-        it('renders empty state with buttons', async () => {
+        it(`renders empty state ${
+          shouldShowEmptyStateButtons ? 'with' : 'without'
+        } buttons`, async () => {
           createComponent({
             handler: emptyHandler,
-            propsData: { shouldShowEmptyStateButtons: true },
+            propsData: { shouldShowEmptyStateButtons },
           });
 
           await waitForPromises();
 
           expect(findEmptyState().props()).toMatchObject({
-            primaryButtonLink: defaultProvide.newProjectPath,
-            primaryButtonText: 'New project',
+            title: "You don't have any projects yet.",
+            description:
+              'Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
+            svgHeight: 144,
+            svgPath: defaultProvide.projectsEmptyStateSvgPath,
           });
+
+          expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons);
         });
-      });
-    });
+      },
+    );
 
     describe('when there are projects', () => {
       beforeEach(() => {
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index 1ec95e4503ad1d2c42c54bd08647a2fb4c39bc9b..4404ebbc9276f2d70507e6db74473bdf0c33eb24 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -3,6 +3,9 @@
 require 'spec_helper'
 
 RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
+  include Devise::Test::ControllerHelpers
+
+  let_it_be(:user) { build_stubbed(:user) }
   let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: '<em>description</em>') }
   let_it_be(:organization) { organization_detail.organization }
   let_it_be(:organization_gid) { 'gid://gitlab/Organizations::Organization/1' }
@@ -27,6 +30,31 @@
     allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path)
     allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path)
     allow(helper).to receive(:preview_markdown_organizations_path).and_return(preview_markdown_organizations_path)
+    allow(helper).to receive(:current_user).and_return(user)
+  end
+
+  shared_examples 'includes that the user can create a group' do |method|
+    it 'returns expected json' do
+      expect(
+        Gitlab::Json.parse(helper.send(method, organization))
+      ).to include('can_create_group' => true)
+    end
+  end
+
+  shared_examples 'includes that the user can create a project' do |method|
+    it 'returns expected json' do
+      expect(
+        Gitlab::Json.parse(helper.send(method, organization))
+      ).to include('can_create_project' => true)
+    end
+  end
+
+  shared_examples 'includes that the organization has groups' do |method|
+    it 'returns expected json' do
+      expect(
+        Gitlab::Json.parse(helper.send(method, organization))
+      ).to include('has_groups' => true)
+    end
   end
 
   describe '#organization_layout_nav' do
@@ -68,13 +96,38 @@
         .and_return(groups_and_projects_organization_path)
     end
 
-    it 'returns expected json' do
+    context 'when the user can create a group' do
+      before do
+        allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true)
+      end
+
+      include_examples 'includes that the user can create a group', 'organization_show_app_data'
+    end
+
+    context 'when the user can create a project' do
+      before do
+        allow(user).to receive(:can_create_project?).and_return(true)
+      end
+
+      include_examples 'includes that the user can create a project', 'organization_show_app_data'
+    end
+
+    context 'when the organization has groups' do
+      before do
+        allow(helper).to receive(:has_groups?).and_return(true)
+      end
+
+      include_examples 'includes that the organization has groups', 'organization_show_app_data'
+    end
+
+    it "includes all other non-conditional data" do
       expect(organization).to receive(:avatar_url).with(size: 128).and_return('avatar.jpg')
+
       expect(
         Gitlab::Json.parse(
           helper.organization_show_app_data(organization)
         )
-      ).to eq(
+      ).to include(
         {
           'organization_gid' => organization_gid,
           'organization' => {
@@ -99,12 +152,36 @@
   end
 
   describe '#organization_groups_and_projects_app_data' do
-    it 'returns expected json' do
+    context 'when the user can create a group' do
+      before do
+        allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true)
+      end
+
+      include_examples 'includes that the user can create a group', 'organization_groups_and_projects_app_data'
+    end
+
+    context 'when the user can create a project' do
+      before do
+        allow(user).to receive(:can_create_project?).and_return(true)
+      end
+
+      include_examples 'includes that the user can create a project', 'organization_groups_and_projects_app_data'
+    end
+
+    context 'when the organization has groups' do
+      before do
+        allow(helper).to receive(:has_groups?).and_return(true)
+      end
+
+      include_examples 'includes that the organization has groups', 'organization_groups_and_projects_app_data'
+    end
+
+    it "includes all other non-conditional data" do
       expect(
         Gitlab::Json.parse(
           helper.organization_groups_and_projects_app_data(organization)
         )
-      ).to eq(
+      ).to include(
         {
           'organization_gid' => organization_gid,
           'new_group_path' => new_group_path,