diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
index 4650390a7ff170269da1bbc80a0b6cf76aa9c71e..337a7f3656ac02ea73d74f3b49797e3507c3ee7c 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -1,20 +1,48 @@
 <script>
+import epicEmptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-epic-md.svg';
+import issuesEmptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-issues-md.svg';
 import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { __ } from '~/locale';
 
 export default {
   components: {
     GlButton,
     GlEmptyState,
   },
-  inject: ['emptyStateSvgPath', 'newIssuePath', 'showNewIssueLink'],
+  inject: {
+    newIssuePath: {
+      default: false,
+    },
+    showNewIssueLink: {
+      default: false,
+    },
+  },
   props: {
     hasSearch: {
       type: Boolean,
-      required: true,
+      required: false,
+      default: false,
+    },
+    isEpic: {
+      type: Boolean,
+      required: false,
+      default: false,
     },
     isOpenTab: {
       type: Boolean,
-      required: true,
+      required: false,
+      default: true,
+    },
+  },
+  computed: {
+    closedTabTitle() {
+      return this.isEpic ? __('There are no closed epics') : __('There are no closed issues');
+    },
+    openTabTitle() {
+      return this.isEpic ? __('There are no open epics') : __('There are no open issues');
+    },
+    svgPath() {
+      return this.isEpic ? epicEmptyStateSvg : issuesEmptyStateSvg;
     },
   },
 };
@@ -25,37 +53,37 @@ export default {
     v-if="hasSearch"
     :description="__('To widen your search, change or remove filters above')"
     :title="__('Sorry, your filter produced no results')"
-    :svg-path="emptyStateSvgPath"
-    :svg-height="150"
+    :svg-path="svgPath"
     data-testid="issuable-empty-state"
   >
     <template #actions>
-      <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
-        {{ __('New issue') }}
-      </gl-button>
+      <slot name="new-issue-button">
+        <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+          {{ __('New issue') }}
+        </gl-button>
+      </slot>
     </template>
   </gl-empty-state>
 
   <gl-empty-state
     v-else-if="isOpenTab"
-    :description="__('To keep this project going, create a new issue')"
-    :title="__('There are no open issues')"
-    :svg-path="emptyStateSvgPath"
-    :svg-height="null"
+    :title="openTabTitle"
+    :svg-path="svgPath"
     data-testid="issuable-empty-state"
   >
     <template #actions>
-      <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
-        {{ __('New issue') }}
-      </gl-button>
+      <slot name="new-issue-button">
+        <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+          {{ __('New issue') }}
+        </gl-button>
+      </slot>
     </template>
   </gl-empty-state>
 
   <gl-empty-state
     v-else
-    :title="__('There are no closed issues')"
-    :svg-path="emptyStateSvgPath"
-    :svg-height="150"
+    :title="closedTabTitle"
+    :svg-path="svgPath"
     data-testid="issuable-empty-state"
   />
 </template>
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
index 3841ba98d654db5feee3354ead46e4faf4379813..6a75e2bdf1e981e00fb3d6d73fa6b91ba46131ab 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -1,4 +1,5 @@
 <script>
+import emptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-issues-md.svg';
 import { GlButton, GlDisclosureDropdown, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
 import { helpPagePath } from '~/helpers/help_page_helper';
 import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
@@ -14,7 +15,9 @@ export default {
       'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
     ),
   },
+  emptyStateSvg,
   issuesHelpPagePath: helpPagePath('user/project/issues/index'),
+  jiraIntegrationPath: helpPagePath('integration/jira/issues', { anchor: 'view-jira-issues' }),
   components: {
     CsvImportExportButtons,
     GlButton,
@@ -29,9 +32,7 @@ export default {
   mixins: [hasNewIssueDropdown()],
   inject: [
     'canCreateProjects',
-    'emptyStateSvgPath',
     'isSignedIn',
-    'jiraIntegrationPath',
     'newIssuePath',
     'newProjectPath',
     'showNewIssueLink',
@@ -89,7 +90,7 @@ export default {
         <div>
           <gl-empty-state
             :title="__('Use issues to collaborate on ideas, solve problems, and plan work')"
-            :svg-path="emptyStateSvgPath"
+            :svg-path="$options.emptyStateSvg"
             :svg-height="150"
             data-testid="issuable-empty-state"
           >
@@ -164,7 +165,7 @@ export default {
             <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
               <template #jiraDocsLink="{ content }">
                 <gl-link
-                  :href="jiraIntegrationPath"
+                  :href="$options.jiraIntegrationPath"
                   :data-track-action="isProject && 'click_jira_int_project_issues_empty_list_page'"
                   :data-track-label="isProject && 'jira_int_project_issues_empty_list'"
                   :data-track-experiment="isProject && 'issues_mrs_empty_state'"
@@ -185,7 +186,7 @@ export default {
   <gl-empty-state
     v-else
     :title="__('Use issues to collaborate on ideas, solve problems, and plan work')"
-    :svg-path="emptyStateSvgPath"
+    :svg-path="$options.emptyStateSvg"
     :svg-height="null"
     :primary-button-text="__('Register / Sign In')"
     :primary-button-link="signInPath"
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues_experiment.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues_experiment.vue
index 41715b6876dea77b47a122d4c009f29452c0eeb1..786d790e195e35966f0a4e65dd9c06849b2dc6b9 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues_experiment.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues_experiment.vue
@@ -8,6 +8,7 @@ import GlCardEmptyStateExperiment from './gl_card_empty_state_experiment.vue';
 
 export default {
   issuesHelpPagePath: helpPagePath('user/project/issues/index'),
+  jiraIntegrationPath: helpPagePath('integration/jira/issues', { anchor: 'view-jira-issues' }),
   components: {
     GlCardEmptyStateExperiment,
     GlButton,
@@ -19,9 +20,6 @@ export default {
     GlModal: GlModalDirective,
   },
   inject: {
-    jiraIntegrationPath: {
-      default: null,
-    },
     newIssuePath: {
       default: null,
     },
@@ -176,7 +174,7 @@ export default {
       >
         <a
           class="gl-text-decoration-none!"
-          :href="jiraIntegrationPath"
+          :href="$options.jiraIntegrationPath"
           data-testid="empty-state-jira-int-link"
           data-track-action="click_jira_int_project_issues_empty_list_page"
           data-track-label="jira_int_project_issues_empty_list"
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 901b965aafce0fbc1d0289cdbd23ce4668849137..ed380cc9862bc60de384dbc93ec3eb5abe1a8e2a 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -68,7 +68,6 @@ export async function mountIssuesListApp() {
     canReadCrmOrganization,
     email,
     emailsHelpPagePath,
-    emptyStateSvgPath,
     exportCsvPath,
     fullPath,
     groupPath,
@@ -89,7 +88,6 @@ export async function mountIssuesListApp() {
     isProject,
     isPublicVisibilityRestricted,
     isSignedIn,
-    jiraIntegrationPath,
     markdownHelpPath,
     maxAttachmentSize,
     newIssuePath,
@@ -126,7 +124,6 @@ export async function mountIssuesListApp() {
       canCreateProjects: parseBoolean(canCreateProjects),
       canReadCrmContact: parseBoolean(canReadCrmContact),
       canReadCrmOrganization: parseBoolean(canReadCrmOrganization),
-      emptyStateSvgPath,
       fullPath,
       projectPath: fullPath,
       groupPath,
@@ -148,7 +145,6 @@ export async function mountIssuesListApp() {
       isProject: parseBoolean(isProject),
       isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
       isSignedIn: parseBoolean(isSignedIn),
-      jiraIntegrationPath,
       newIssuePath,
       newProjectPath,
       releasesPath,
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index a2f11a728e4b6896327b30463551b7077405fa8c..b8a8ba56932828b928b9ecae3265fed07581a1c0 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,5 @@
 <script>
-import { GlFilteredSearchToken } from '@gitlab/ui';
+import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
 import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -34,15 +34,25 @@ export default {
   issuableListTabs,
   sortOptions,
   components: {
+    GlLoadingIcon,
     IssuableList,
     IssueCardStatistics,
     IssueCardTimeInfo,
   },
   inject: ['fullPath', 'initialSort', 'isSignedIn', 'workItemType'],
+  props: {
+    eeCreatedWorkItemsCount: {
+      type: Number,
+      required: false,
+      default: 0,
+    },
+  },
   data() {
     return {
       error: undefined,
       filterTokens: [],
+      hasAnyIssues: false,
+      isInitialAllCountSet: false,
       pageInfo: {},
       pageParams: getInitialPageParams(),
       sortKey: deriveSortKey({ sort: this.initialSort, sortMap: urlSortParams }),
@@ -77,6 +87,11 @@ export default {
           [STATUS_CLOSED]: closed,
           [STATUS_ALL]: all,
         };
+
+        if (!this.isInitialAllCountSet) {
+          this.hasAnyIssues = Boolean(all);
+          this.isInitialAllCountSet = true;
+        }
       },
       error(error) {
         this.error = s__(
@@ -90,6 +105,12 @@ export default {
     apiFilterParams() {
       return convertToApiParams(this.filterTokens);
     },
+    hasSearch() {
+      return Boolean(this.searchQuery);
+    },
+    isOpenTab() {
+      return this.state === STATUS_OPEN;
+    },
     searchQuery() {
       return convertToSearchQuery(this.filterTokens);
     },
@@ -137,6 +158,15 @@ export default {
       return this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage;
     },
   },
+  watch: {
+    eeCreatedWorkItemsCount() {
+      // Only reset isInitialAllCountSet when there's no issues to minimize unmounting IssuableList
+      if (!this.hasAnyIssues) {
+        this.isInitialAllCountSet = false;
+      }
+      this.$apollo.queries.workItems.refetch();
+    },
+  },
   methods: {
     getStatus(issue) {
       return issue.state === STATE_CLOSED ? __('Closed') : undefined;
@@ -199,7 +229,10 @@ export default {
 </script>
 
 <template>
+  <gl-loading-icon v-if="!isInitialAllCountSet && !error" class="gl-mt-5" size="lg" />
+
   <issuable-list
+    v-else-if="hasAnyIssues || error"
     :current-tab="state"
     :error="error"
     :has-next-page="pageInfo.hasNextPage"
@@ -239,8 +272,16 @@ export default {
       <issue-card-statistics :issue="issuable" />
     </template>
 
+    <template #empty-state>
+      <slot name="list-empty-state" :has-search="hasSearch" :is-open-tab="isOpenTab"></slot>
+    </template>
+
     <template #list-body>
       <slot name="list-body"></slot>
     </template>
   </issuable-list>
+
+  <div v-else>
+    <slot name="page-empty-state"></slot>
+  </div>
 </template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 886400835cc3253dd31769b70654c497ea4124a8..447145601b6e6455bde074c745c120dca271d554 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -20,6 +20,7 @@ export const mountWorkItemsListApp = () => {
     hasIssueWeightsFeature,
     initialSort,
     isSignedIn,
+    showNewIssueLink,
     workItemType,
   } = el.dataset;
 
@@ -37,6 +38,7 @@ export const mountWorkItemsListApp = () => {
       initialSort,
       isSignedIn: parseBoolean(isSignedIn),
       isGroup: true,
+      showNewIssueLink: parseBoolean(showNewIssueLink),
       workItemType,
     },
     render: (createComponent) => createComponent(WorkItemsListApp),
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 9e9c0789a9ecb33d107b0e56fad6ced6b6f46e69..b1efa39fba10e1813d6e663b51715a95a9fdf460 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -135,14 +135,12 @@ def common_issues_list_data(namespace, current_user)
     {
       autocomplete_award_emojis_path: autocomplete_award_emojis_path,
       calendar_path: url_for(safe_params.merge(calendar_url_options)),
-      empty_state_svg_path: image_path('illustrations/empty-state/empty-service-desk-md.svg'),
       full_path: namespace.full_path,
       initial_sort: current_user&.user_preference&.issues_sort,
       is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
       is_public_visibility_restricted:
         Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
       is_signed_in: current_user.present?.to_s,
-      jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
       rss_path: url_for(safe_params.merge(rss_url_options)),
       sign_in_path: new_user_session_path,
       has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index a1364c312f9c07caf2858dd37800a9ec5c73d785..7b997597d7a7a03cbe06f6e30bc4a258a7099417 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -20,7 +20,8 @@ def work_items_list_data(group, current_user)
     {
       full_path: group.full_path,
       initial_sort: current_user&.user_preference&.issues_sort,
-      is_signed_in: current_user.present?.to_s
+      is_signed_in: current_user.present?.to_s,
+      show_new_issue_link: can?(current_user, :create_work_item, group).to_s
     }
   end
 end
diff --git a/ee/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/ee/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index b2d1321f94efbcf72ba38867341519150e298ede..50c9f5f708b838c7ad9b02902563d7dce556a48b 100644
--- a/ee/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/ee/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,39 +1,74 @@
 <script>
+import emptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-epic-md.svg';
+import { GlEmptyState } from '@gitlab/ui';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
 import { WORK_ITEM_TYPE_ENUM_EPIC } from '~/work_items/constants';
 import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue';
 import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
 
 export default {
+  emptyStateSvg,
   WORK_ITEM_TYPE_ENUM_EPIC,
   components: {
     CreateWorkItemModal,
+    EmptyStateWithAnyIssues,
+    GlEmptyState,
     WorkItemsListApp,
   },
-  inject: ['hasEpicsFeature'],
+  inject: ['hasEpicsFeature', 'showNewIssueLink'],
   data() {
     return {
-      showEpicCreationForm: false,
+      createdWorkItemsCount: 0,
     };
   },
   methods: {
-    handleCreated({ workItem }) {
-      if (workItem.id) {
-        // Refresh results on list
-        this.showEpicCreationForm = false;
-        this.$refs.workItemsListApp.$apollo.queries.workItems.refetch();
-      }
+    handleCreated() {
+      this.createdWorkItemsCount += 1;
     },
   },
 };
 </script>
 
 <template>
-  <work-items-list-app ref="workItemsListApp">
-    <template v-if="hasEpicsFeature" #nav-actions>
+  <work-items-list-app :ee-created-work-items-count="createdWorkItemsCount">
+    <template v-if="hasEpicsFeature && showNewIssueLink" #nav-actions>
       <create-work-item-modal
         class="gl-flex-grow-1"
         :work-item-type-name="$options.WORK_ITEM_TYPE_ENUM_EPIC"
+        @workItemCreated="handleCreated"
       />
     </template>
+    <template v-if="hasEpicsFeature" #list-empty-state="{ hasSearch, isOpenTab }">
+      <empty-state-with-any-issues :has-search="hasSearch" is-epic :is-open-tab="isOpenTab">
+        <template v-if="showNewIssueLink" #new-issue-button>
+          <create-work-item-modal
+            class="gl-flex-grow-1"
+            :work-item-type-name="$options.WORK_ITEM_TYPE_ENUM_EPIC"
+            @workItemCreated="handleCreated"
+          />
+        </template>
+      </empty-state-with-any-issues>
+    </template>
+    <template v-if="hasEpicsFeature" #page-empty-state>
+      <gl-empty-state
+        :description="
+          __('Track groups of issues that share a theme, across projects and milestones')
+        "
+        :svg-path="$options.emptyStateSvg"
+        :title="
+          __(
+            'Epics let you manage your portfolio of projects more efficiently and with less effort',
+          )
+        "
+      >
+        <template v-if="showNewIssueLink" #actions>
+          <create-work-item-modal
+            class="gl-flex-grow-1"
+            :work-item-type-name="$options.WORK_ITEM_TYPE_ENUM_EPIC"
+            @workItemCreated="handleCreated"
+          />
+        </template>
+      </gl-empty-state>
+    </template>
   </work-items-list-app>
 </template>
diff --git a/ee/spec/frontend/issues/list/components/issues_list_app_spec.js b/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
index 04e01d4fa7060fc63767146b7a55c5f4398d18f6..447f9315ac888616700913347648a921cf83ee81 100644
--- a/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -62,7 +62,6 @@ describe('EE IssuesListApp component', () => {
     canCreateProjects: false,
     canReadCrmContact: false,
     canReadCrmOrganization: false,
-    emptyStateSvgPath: 'empty-state.svg',
     exportCsvPath: 'export/csv/path',
     fullPath: 'path/to/project',
     groupPath: 'group/path',
@@ -82,7 +81,6 @@ describe('EE IssuesListApp component', () => {
     isProject: true,
     isPublicVisibilityRestricted: false,
     isSignedIn: true,
-    jiraIntegrationPath: 'jira/integration/path',
     newIssuePath: 'new/issue/path',
     newProjectPath: 'new/project/path',
     releasesPath: 'releases/path',
diff --git a/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 833722619594c821418c43adfa1872fb993b2c03..17e1b819faeeb1dd299a8038027bd35f7175a346 100644
--- a/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/ee/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -1,32 +1,96 @@
+import { GlEmptyState } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
 import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
+import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue';
 import EEWorkItemsListApp from 'ee/work_items/list/components/work_items_list_app.vue';
 
 describe('WorkItemsListApp EE component', () => {
+  /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
   let wrapper;
 
   const findCreateWorkItemModal = () => wrapper.findComponent(CreateWorkItemModal);
+  const findListEmptyState = () => wrapper.findComponent(EmptyStateWithAnyIssues);
+  const findPageEmptyState = () => wrapper.findComponent(GlEmptyState);
+  const findWorkItemsListApp = () => wrapper.findComponent(WorkItemsListApp);
 
-  const mountComponent = ({ hasEpicsFeature = false } = {}) => {
+  const mountComponent = ({ hasEpicsFeature = true, showNewIssueLink = true } = {}) => {
     wrapper = shallowMount(EEWorkItemsListApp, {
       provide: {
         hasEpicsFeature,
+        showNewIssueLink,
       },
     });
   };
 
-  it('renders create work item modal when epics feature available', () => {
-    mountComponent({ hasEpicsFeature: true });
+  describe('create-work-item modal', () => {
+    describe.each`
+      hasEpicsFeature | showNewIssueLink | exists
+      ${false}        | ${false}         | ${false}
+      ${true}         | ${false}         | ${false}
+      ${false}        | ${true}          | ${false}
+      ${true}         | ${true}          | ${true}
+    `(
+      'when hasEpicsFeature=$hasEpicsFeature and showNewIssueLink=$showNewIssueLink',
+      ({ hasEpicsFeature, showNewIssueLink, exists }) => {
+        it(`${exists ? 'renders' : 'does not render'}`, () => {
+          mountComponent({ hasEpicsFeature, showNewIssueLink });
 
-    expect(findCreateWorkItemModal().props()).toEqual({
-      workItemTypeName: 'EPIC',
-      asDropdownItem: false,
+          expect(findCreateWorkItemModal().exists()).toBe(exists);
+        });
+      },
+    );
+
+    describe('when "workItemCreated" event is emitted', () => {
+      it('increments `eeCreatedWorkItemsCount` prop on WorkItemsListApp', async () => {
+        mountComponent();
+
+        expect(findWorkItemsListApp().props('eeCreatedWorkItemsCount')).toBe(0);
+
+        findCreateWorkItemModal().vm.$emit('workItemCreated');
+        await nextTick();
+
+        expect(findWorkItemsListApp().props('eeCreatedWorkItemsCount')).toBe(1);
+      });
     });
   });
 
-  it('does not render modal when epics feature not available', () => {
-    mountComponent({ hasEpicsFeature: false });
+  describe('empty states', () => {
+    describe('when hasEpicsFeature=true', () => {
+      beforeEach(() => {
+        mountComponent({ hasEpicsFeature: true });
+      });
+
+      it('renders list empty state', () => {
+        expect(findListEmptyState().props()).toEqual({
+          hasSearch: false,
+          isEpic: true,
+          isOpenTab: true,
+        });
+      });
 
-    expect(findCreateWorkItemModal().exists()).toBe(false);
+      it('renders page empty state', () => {
+        expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+          description: 'Track groups of issues that share a theme, across projects and milestones',
+          title:
+            'Epics let you manage your portfolio of projects more efficiently and with less effort',
+        });
+      });
+    });
+
+    describe('when hasEpicsFeature=false', () => {
+      beforeEach(() => {
+        mountComponent({ hasEpicsFeature: false });
+      });
+
+      it('does not render list empty state', () => {
+        expect(findListEmptyState().exists()).toBe(false);
+      });
+
+      it('does not render page empty state', () => {
+        expect(findPageEmptyState().exists()).toBe(false);
+      });
+    });
   });
 });
diff --git a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
index 8f632ed12c55619e358f36d5776a918aef371e35..78f0cb53e97faa735efaeb3fac02fe57413c36a5 100644
--- a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
@@ -11,11 +11,11 @@ describe('EmptyStateWithAnyIssues component', () => {
     wrapper = shallowMount(EmptyStateWithAnyIssues, {
       propsData: {
         hasSearch: true,
+        isEpic: false,
         isOpenTab: true,
         ...props,
       },
       provide: {
-        emptyStateSvgPath: 'empty/state/svg/path',
         newIssuePath: 'new/issue/path',
         showNewIssueLink: false,
       },
@@ -23,42 +23,46 @@ describe('EmptyStateWithAnyIssues component', () => {
   };
 
   describe('when there is a search (with no results)', () => {
-    beforeEach(() => {
+    it('shows empty state', () => {
       mountComponent({ hasSearch: true });
-    });
 
-    it('shows empty state', () => {
       expect(findGlEmptyState().props()).toMatchObject({
         description: 'To widen your search, change or remove filters above',
         title: 'Sorry, your filter produced no results',
-        svgPath: 'empty/state/svg/path',
       });
     });
   });
 
   describe('when "Open" tab is active', () => {
-    beforeEach(() => {
+    it('shows empty state', () => {
       mountComponent({ hasSearch: false, isOpenTab: true });
-    });
 
-    it('shows empty state', () => {
-      expect(findGlEmptyState().props()).toMatchObject({
-        description: 'To keep this project going, create a new issue',
-        title: 'There are no open issues',
-        svgPath: 'empty/state/svg/path',
-      });
+      expect(findGlEmptyState().props('title')).toBe('There are no open issues');
     });
   });
 
   describe('when "Closed" tab is active', () => {
-    beforeEach(() => {
+    it('shows empty state', () => {
       mountComponent({ hasSearch: false, isOpenTab: false });
+
+      expect(findGlEmptyState().props('title')).toBe('There are no closed issues');
     });
+  });
 
-    it('shows empty state', () => {
-      expect(findGlEmptyState().props()).toMatchObject({
-        title: 'There are no closed issues',
-        svgPath: 'empty/state/svg/path',
+  describe('when epic', () => {
+    describe('when "Open" tab is active', () => {
+      it('shows empty state', () => {
+        mountComponent({ hasSearch: false, isEpic: true, isOpenTab: true });
+
+        expect(findGlEmptyState().props('title')).toBe('There are no open epics');
+      });
+    });
+
+    describe('when "Closed" tab is active', () => {
+      it('shows empty state', () => {
+        mountComponent({ hasSearch: false, isEpic: true, isOpenTab: false });
+
+        expect(findGlEmptyState().props('title')).toBe('There are no closed epics');
       });
     });
   });
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index 956fd485d28b3720a545db7de2d934f65c4fa2f8..f0790d83f694c875e785f851558c1ae6b1b8c1c2 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -17,10 +17,8 @@ describe('EmptyStateWithoutAnyIssues component', () => {
 
   const defaultProvide = {
     canCreateProjects: false,
-    emptyStateSvgPath: 'empty/state/svg/path',
     fullPath: 'full/path',
     isSignedIn: true,
-    jiraIntegrationPath: 'jira/integration/path',
     newIssuePath: 'new/issue/path',
     newProjectPath: 'new/project/path',
     showNewIssueLink: false,
@@ -64,10 +62,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
       it('renders empty state', () => {
         mountComponent();
 
-        expect(findGlEmptyState().props()).toMatchObject({
-          title: 'Use issues to collaborate on ideas, solve problems, and plan work',
-          svgPath: defaultProvide.emptyStateSvgPath,
-        });
+        expect(findGlEmptyState().props('title')).toBe(
+          'Use issues to collaborate on ideas, solve problems, and plan work',
+        );
       });
 
       describe('description', () => {
@@ -280,7 +277,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
       });
 
       it('renders Jira integration docs link', () => {
-        expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+        expect(findJiraDocsLink().attributes('href')).toBe(
+          '/help/integration/jira/issues#view-jira-issues',
+        );
       });
     });
 
@@ -308,7 +307,6 @@ describe('EmptyStateWithoutAnyIssues component', () => {
     it('renders empty state', () => {
       expect(findGlEmptyState().props()).toMatchObject({
         title: 'Use issues to collaborate on ideas, solve problems, and plan work',
-        svgPath: defaultProvide.emptyStateSvgPath,
         primaryButtonText: 'Register / Sign In',
         primaryButtonLink: defaultProvide.signInPath,
       });
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 4f8285537de9f5259aa17eede6423b7b6cc2f04d..c4e86e3e711aaa70b26aa07dfba109f30ab63834 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -112,7 +112,6 @@ describe('CE IssuesListApp component', () => {
     canCreateProjects: false,
     canReadCrmContact: false,
     canReadCrmOrganization: false,
-    emptyStateSvgPath: 'empty-state.svg',
     exportCsvPath: 'export/csv/path',
     fullPath: 'path/to/project',
     hasAnyIssues: true,
@@ -129,7 +128,6 @@ describe('CE IssuesListApp component', () => {
     isProject: true,
     isPublicVisibilityRestricted: false,
     isSignedIn: true,
-    jiraIntegrationPath: 'jira/integration/path',
     newIssuePath: 'new/issue/path',
     newProjectPath: 'new/project/path',
     releasesPath: 'releases/path',
@@ -610,6 +608,7 @@ describe('CE IssuesListApp component', () => {
         it('shows EmptyStateWithAnyIssues empty state', () => {
           expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
             hasSearch: false,
+            isEpic: false,
             isOpenTab: true,
           });
         });
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index dcb532c7f770c220846444f96d80283bbca93aee..89644df305da7696ed84669d267916394d9c2ed2 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import { cloneDeep } from 'lodash';
 import Vue, { nextTick } from 'vue';
@@ -33,6 +34,7 @@ jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
 jest.mock('~/sentry/sentry_browser_wrapper');
 
 describe('WorkItemsListApp component', () => {
+  /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
   let wrapper;
 
   Vue.use(VueApollo);
@@ -63,44 +65,62 @@ describe('WorkItemsListApp component', () => {
     });
   };
 
-  it('renders IssuableList component', () => {
+  it('renders loading icon when initially fetching work items', () => {
     mountComponent();
 
-    expect(findIssuableList().props()).toMatchObject({
-      currentTab: STATUS_OPEN,
-      error: '',
-      initialSortBy: CREATED_DESC,
-      issuables: [],
-      issuablesLoading: true,
-      namespace: 'work-items',
-      recentSearchesStorageKey: 'issues',
-      showWorkItemTypeIcon: true,
-      sortOptions,
-      tabs: WorkItemsListApp.issuableListTabs,
-    });
+    expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
   });
 
-  it('renders tab counts', async () => {
-    mountComponent();
-    await waitForPromises();
+  describe('when work items are fetched', () => {
+    beforeEach(async () => {
+      mountComponent();
+      await waitForPromises();
+    });
 
-    expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
-      all: 3,
-      closed: 1,
-      opened: 2,
+    it('renders IssuableList component', () => {
+      expect(findIssuableList().props()).toMatchObject({
+        currentTab: STATUS_OPEN,
+        error: '',
+        initialSortBy: CREATED_DESC,
+        namespace: 'work-items',
+        recentSearchesStorageKey: 'issues',
+        showWorkItemTypeIcon: true,
+        sortOptions,
+        tabs: WorkItemsListApp.issuableListTabs,
+      });
     });
-  });
 
-  it('renders IssueCardStatistics component', () => {
-    mountComponent();
+    it('renders tab counts', () => {
+      expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
+        all: 3,
+        closed: 1,
+        opened: 2,
+      });
+    });
 
-    expect(findIssueCardStatistics().exists()).toBe(true);
-  });
+    it('renders IssueCardStatistics component', () => {
+      expect(findIssueCardStatistics().exists()).toBe(true);
+    });
 
-  it('renders IssueCardTimeInfo component', () => {
-    mountComponent();
+    it('renders IssueCardTimeInfo component', () => {
+      expect(findIssueCardTimeInfo().exists()).toBe(true);
+    });
+
+    it('renders work items', () => {
+      expect(findIssuableList().props('issuables')).toEqual(
+        groupWorkItemsQueryResponse.data.group.workItems.nodes,
+      );
+    });
 
-    expect(findIssueCardTimeInfo().exists()).toBe(true);
+    it('calls query to fetch work items', () => {
+      expect(defaultQueryHandler).toHaveBeenCalledWith({
+        fullPath: 'full/path',
+        sort: CREATED_DESC,
+        state: STATUS_OPEN,
+        firstPageSize: 20,
+        types: [null],
+      });
+    });
   });
 
   describe('pagination controls', () => {
@@ -122,53 +142,30 @@ describe('WorkItemsListApp component', () => {
     });
   });
 
-  it('renders work items', async () => {
-    mountComponent();
-    await waitForPromises();
-
-    expect(findIssuableList().props('issuables')).toEqual(
-      groupWorkItemsQueryResponse.data.group.workItems.nodes,
-    );
-  });
+  describe('when workItemType is provided', () => {
+    it('filters work items by workItemType', () => {
+      const type = 'EPIC';
+      mountComponent({ provide: { workItemType: type } });
 
-  it('fetches work items', () => {
-    mountComponent();
-
-    expect(defaultQueryHandler).toHaveBeenCalledWith({
-      fullPath: 'full/path',
-      sort: CREATED_DESC,
-      state: STATUS_OPEN,
-      firstPageSize: 20,
-      types: [null],
-    });
-  });
-
-  it('filters work items by workItemType', () => {
-    const type = 'EPIC';
-    mountComponent({
-      provide: {
-        workItemType: type,
-      },
-    });
-
-    expect(defaultQueryHandler).toHaveBeenCalledWith({
-      fullPath: 'full/path',
-      sort: CREATED_DESC,
-      state: STATUS_OPEN,
-      firstPageSize: 20,
-      types: [type],
+      expect(defaultQueryHandler).toHaveBeenCalledWith({
+        fullPath: 'full/path',
+        sort: CREATED_DESC,
+        state: STATUS_OPEN,
+        firstPageSize: 20,
+        types: [type],
+      });
     });
   });
 
   describe('when there is an error fetching work items', () => {
+    const message = 'Something went wrong when fetching work items. Please try again.';
+
     beforeEach(async () => {
       mountComponent({ queryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
       await waitForPromises();
     });
 
     it('renders an error message', () => {
-      const message = 'Something went wrong when fetching work items. Please try again.';
-
       expect(findIssuableList().props('error')).toBe(message);
       expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
     });
@@ -177,7 +174,22 @@ describe('WorkItemsListApp component', () => {
       findIssuableList().vm.$emit('dismiss-alert');
       await nextTick();
 
-      expect(findIssuableList().props('error')).toBe('');
+      expect(wrapper.text()).not.toContain(message);
+    });
+  });
+
+  describe('watcher', () => {
+    describe('when eeCreatedWorkItemsCount is updated', () => {
+      it('refetches work items', async () => {
+        mountComponent();
+        await waitForPromises();
+
+        expect(defaultQueryHandler).toHaveBeenCalledTimes(1);
+
+        await wrapper.setProps({ eeCreatedWorkItemsCount: 1 });
+
+        expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
+      });
     });
   });
 
@@ -189,7 +201,7 @@ describe('WorkItemsListApp component', () => {
       avatar_url: 'avatar/url',
     };
 
-    beforeEach(() => {
+    beforeEach(async () => {
       window.gon = {
         current_user_id: mockCurrentUser.id,
         current_user_fullname: mockCurrentUser.name,
@@ -197,6 +209,7 @@ describe('WorkItemsListApp component', () => {
         current_user_avatar_url: mockCurrentUser.avatar_url,
       };
       mountComponent();
+      await waitForPromises();
     });
 
     it('renders all tokens', () => {
@@ -228,6 +241,7 @@ describe('WorkItemsListApp component', () => {
     describe('when "filter" event is emitted by IssuableList', () => {
       it('fetches filtered work items', async () => {
         mountComponent();
+        await waitForPromises();
 
         findIssuableList().vm.$emit('filter', [
           { type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
@@ -280,6 +294,7 @@ describe('WorkItemsListApp component', () => {
           } else {
             mountComponent();
           }
+          await waitForPromises();
 
           findIssuableList().vm.$emit('sort', sortKey);
           await waitForPromises();
@@ -291,9 +306,10 @@ describe('WorkItemsListApp component', () => {
       );
 
       describe('when user is signed in', () => {
-        it('calls mutation to save sort preference', () => {
+        it('calls mutation to save sort preference', async () => {
           const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
           mountComponent({ sortPreferenceMutationResponse: mutationMock });
+          await waitForPromises();
 
           findIssuableList().vm.$emit('sort', UPDATED_DESC);
 
@@ -305,6 +321,7 @@ describe('WorkItemsListApp component', () => {
             .fn()
             .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
           mountComponent({ sortPreferenceMutationResponse: mutationMock });
+          await waitForPromises();
 
           findIssuableList().vm.$emit('sort', UPDATED_DESC);
           await waitForPromises();
@@ -314,12 +331,13 @@ describe('WorkItemsListApp component', () => {
       });
 
       describe('when user is signed out', () => {
-        it('does not call mutation to save sort preference', () => {
+        it('does not call mutation to save sort preference', async () => {
           const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
           mountComponent({
             provide: { isSignedIn: false },
             sortPreferenceMutationResponse: mutationMock,
           });
+          await waitForPromises();
 
           findIssuableList().vm.$emit('sort', CREATED_DESC);
 
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index aad2369ca6ec0b301395718b32125d546bde5476..c0feb5796c5df86a4a8fd48332a1be18839c639f 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -3970,6 +3970,29 @@ export const groupWorkItemsQueryResponse = {
   },
 };
 
+export const emptyGroupWorkItemsQueryResponse = {
+  data: {
+    group: {
+      id: 'gid://gitlab/Group/3',
+      workItemStateCounts: {
+        all: 0,
+        closed: 0,
+        opened: 0,
+      },
+      workItems: {
+        pageInfo: {
+          hasNextPage: false,
+          hasPreviousPage: false,
+          startCursor: 'startCursor',
+          endCursor: 'endCursor',
+          __typename: 'PageInfo',
+        },
+        nodes: [],
+      },
+    },
+  },
+};
+
 export const updateWorkItemMutationResponseFactory = (options) => {
   const response = workItemResponseFactory(options);
   return {
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index f26cac05490008e401ef6979bb1eefe6b7130a0c..0a0d659f8e2c2b9f882e3078b806a88136ee2580 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -213,7 +213,6 @@
         can_import_issues: 'true',
         email: current_user&.notification_email_or_default,
         emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
-        empty_state_svg_path: '#',
         export_csv_path: export_csv_project_issues_path(project),
         full_path: project.full_path,
         has_any_issues: project_issues(project).exists?.to_s,
@@ -224,7 +223,6 @@
         is_project: 'true',
         is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
         is_signed_in: current_user.present?.to_s,
-        jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
         markdown_help_path: help_page_path('user/markdown'),
         max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
         new_issue_path: new_project_issue_path(project),
@@ -281,12 +279,10 @@
         autocomplete_award_emojis_path: autocomplete_award_emojis_path,
         calendar_path: '#',
         can_create_projects: 'true',
-        empty_state_svg_path: '#',
         full_path: group.full_path,
         has_any_issues: false.to_s,
         has_any_projects: true.to_s,
         is_signed_in: current_user.present?.to_s,
-        jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
         new_project_path: new_project_path(namespace_id: group.id),
         rss_path: '#',
         sign_in_path: new_user_session_path,
diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb
index 587faa8dfca57677e1fc7bd61465998e8cbb6c75..80039f4cc9ca682b28ce43fc7eb2ee3ddab96cdf 100644
--- a/spec/helpers/work_items_helper_spec.rb
+++ b/spec/helpers/work_items_helper_spec.rb
@@ -32,12 +32,14 @@
 
     it 'returns expected data' do
       allow(helper).to receive(:current_user).and_return(current_user)
+      allow(helper).to receive(:can?).and_return(true)
 
       expect(work_items_list_data).to include(
         {
           full_path: group.full_path,
           initial_sort: current_user&.user_preference&.issues_sort,
-          is_signed_in: current_user.present?.to_s
+          is_signed_in: current_user.present?.to_s,
+          show_new_issue_link: 'true'
         }
       )
     end