diff --git a/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_active_list.vue b/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_active_list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f33d845210254fbcbb70caf55c37da92cb259549
--- /dev/null
+++ b/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_active_list.vue
@@ -0,0 +1,65 @@
+<script>
+import mostActiveRunnersQuery from 'ee/ci/runner/graphql/performance/most_active_runners.query.graphql';
+import RunnerActiveList from 'ee/ci/runner/components/runner_active_list.vue';
+
+import { captureException } from '~/ci/runner/sentry_utils';
+import { fetchPolicies } from '~/lib/graphql';
+import { createAlert } from '~/alert';
+import { I18N_FETCH_ERROR, JOBS_ROUTE_PATH } from '~/ci/runner/constants';
+
+export default {
+  name: 'AdminRunnerActiveList',
+  components: {
+    RunnerActiveList,
+  },
+  data() {
+    return {
+      activeRunners: [],
+    };
+  },
+  apollo: {
+    activeRunners: {
+      query: mostActiveRunnersQuery,
+      fetchPolicy: fetchPolicies.NETWORK_ONLY,
+      update({ runners }) {
+        const items = runners?.nodes || [];
+        return (
+          items
+            // The backend does not filter out inactive runners, but
+            // showing them can be confusing for users. Ignore runners
+            // with no active jobs.
+            .filter((item) => item.runningJobCount > 0)
+            .map((item) => {
+              const { adminUrl, ...runner } = item;
+              return {
+                ...runner,
+                jobsUrl: this.jobsUrl(adminUrl),
+              };
+            })
+        );
+      },
+      error(error) {
+        createAlert({ message: I18N_FETCH_ERROR });
+
+        captureException({ error, component: this.$options.name });
+      },
+    },
+  },
+  computed: {
+    loading() {
+      return this.$apollo.queries.activeRunners.loading;
+    },
+  },
+  methods: {
+    jobsUrl(adminUrl) {
+      const url = new URL(adminUrl);
+      url.hash = JOBS_ROUTE_PATH;
+
+      return url.href;
+    },
+  },
+};
+</script>
+<template>
+  <runner-active-list :active-runners="activeRunners" :loading="loading" />
+</template>
diff --git a/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app.vue b/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app.vue
index ba08a7864fb204128a72284e240177dc92a8c442..00b755e56f4a3ee8dedfce7230331c8308bd4778 100644
--- a/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app.vue
+++ b/ee/app/assets/javascripts/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app.vue
@@ -5,18 +5,19 @@ import RunnerDashboardStatOnline from '../components/runner_dashboard_stat_onlin
 import RunnerDashboardStatOffline from '../components/runner_dashboard_stat_offline.vue';
 import RunnerUsage from '../components/runner_usage.vue';
 import RunnerJobFailures from '../components/runner_job_failures.vue';
-import RunnerActiveList from '../components/runner_active_list.vue';
 import RunnerWaitTimes from '../components/runner_wait_times.vue';
 
+import AdminRunnerActiveList from './admin_runners_active_list.vue';
+
 export default {
   components: {
     GlButton,
+    AdminRunnerActiveList,
     RunnerListHeader,
     RunnerDashboardStatOnline,
     RunnerDashboardStatOffline,
     RunnerUsage,
     RunnerJobFailures,
-    RunnerActiveList,
     RunnerWaitTimes,
   },
   inject: {
@@ -67,7 +68,7 @@ export default {
           <runner-job-failures v-else class="gl-flex-basis-full" />
         </div>
 
-        <runner-active-list class="runners-dashboard-third-gap-4 gl-mb-4" />
+        <admin-runner-active-list class="runners-dashboard-third-gap-4 gl-mb-4" />
       </div>
     </div>
     <runner-wait-times class="gl-mb-4" />
diff --git a/ee/app/assets/javascripts/ci/runner/components/runner_active_list.vue b/ee/app/assets/javascripts/ci/runner/components/runner_active_list.vue
index 57bac5a90c20148f91ab9bc254da446cde3edd52..3f8bfd2f2ffb458c4c2fc0922c1dfb570aa599b4 100644
--- a/ee/app/assets/javascripts/ci/runner/components/runner_active_list.vue
+++ b/ee/app/assets/javascripts/ci/runner/components/runner_active_list.vue
@@ -1,14 +1,7 @@
 <script>
-import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
 import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
 import { formatNumber, s__ } from '~/locale';
 
-import mostActiveRunnersQuery from 'ee/ci/runner/graphql/performance/most_active_runners.graphql';
-
-import { captureException } from '~/ci/runner/sentry_utils';
-import { fetchPolicies } from '~/lib/graphql';
-import { createAlert } from '~/alert';
-import { I18N_FETCH_ERROR, JOBS_ROUTE_PATH } from '~/ci/runner/constants';
 import { tableField } from '~/ci/runner/utils';
 import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
 
@@ -23,41 +16,19 @@ export default {
     GlSkeletonLoader,
     RunnerFullName,
   },
-  data() {
-    return {
-      activeRunners: [],
-    };
-  },
-  apollo: {
+  props: {
     activeRunners: {
-      query: mostActiveRunnersQuery,
-      fetchPolicy: fetchPolicies.NETWORK_ONLY,
-      update({ runners }) {
-        // The backend does not filter out inactive runners, but
-        // showing them can be confusing for users. Ignore runners
-        // with no active jobs.
-        const items = runners?.nodes || [];
-        return items.filter((item) => item.runningJobCount > 0);
-      },
-      error(error) {
-        createAlert({ message: I18N_FETCH_ERROR });
-
-        captureException({ error, component: this.$options.name });
-      },
+      type: Array,
+      default: () => [],
+      required: false,
     },
-  },
-  computed: {
-    loading() {
-      return this.$apollo.queries.activeRunners.loading;
+    loading: {
+      type: Boolean,
+      default: false,
+      required: false,
     },
   },
   methods: {
-    jobsUrl({ adminUrl }) {
-      const url = new URL(adminUrl);
-      url.hash = JOBS_ROUTE_PATH;
-
-      return url.href;
-    },
     formatNumber,
   },
   fields: [
@@ -70,7 +41,6 @@ export default {
     }),
   ],
   CI_ICON_STATUS: { group: 'running', icon: 'status_running' },
-  EMPTY_STATE_SVG_URL,
 };
 </script>
 <template>
@@ -94,7 +64,7 @@ export default {
         <runner-full-name :runner="item" />
       </template>
       <template #cell(runningJobCount)="{ item = {}, value }">
-        <gl-link :href="jobsUrl(item)">
+        <gl-link :href="item.jobsUrl">
           <ci-icon :status="$options.CI_ICON_STATUS" />
           {{ formatNumber(value) }}
         </gl-link>
diff --git a/ee/app/assets/javascripts/ci/runner/graphql/performance/most_active_runners.graphql b/ee/app/assets/javascripts/ci/runner/graphql/performance/most_active_runners.query.graphql
similarity index 100%
rename from ee/app/assets/javascripts/ci/runner/graphql/performance/most_active_runners.graphql
rename to ee/app/assets/javascripts/ci/runner/graphql/performance/most_active_runners.query.graphql
diff --git a/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_active_list_spec.js b/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_active_list_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dfea760a25a21c368de7021e35873db1d81b6bcc
--- /dev/null
+++ b/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_active_list_spec.js
@@ -0,0 +1,123 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerActiveList from 'ee/ci/runner/components/runner_active_list.vue';
+
+import AdminRunnersActiveList from 'ee/ci/runner/admin_runners_dashboard/admin_runners_active_list.vue';
+import mostActiveRunnersQuery from 'ee/ci/runner/graphql/performance/most_active_runners.query.graphql';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { JOBS_ROUTE_PATH } from '~/ci/runner/constants';
+
+import { mostActiveRunnersData } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+const mostActiveRunners = mostActiveRunnersData.data.runners.nodes;
+const [mockRunner, mockRunner2] = mostActiveRunners;
+
+Vue.use(VueApollo);
+
+describe('AdminRunnersActiveList', () => {
+  let wrapper;
+  let mostActiveRunnersHandler;
+
+  const findRunnersActiveList = () => wrapper.findComponent(RunnerActiveList);
+
+  const createComponent = () => {
+    wrapper = shallowMountExtended(AdminRunnersActiveList, {
+      apolloProvider: createMockApollo([[mostActiveRunnersQuery, mostActiveRunnersHandler]]),
+    });
+  };
+
+  beforeEach(() => {
+    mostActiveRunnersHandler = jest.fn();
+  });
+
+  it('Requests most active runners', () => {
+    createComponent();
+
+    expect(mostActiveRunnersHandler).toHaveBeenCalledTimes(1);
+  });
+
+  describe('When loading data', () => {
+    it('should show a loading skeleton', () => {
+      createComponent();
+
+      expect(findRunnersActiveList().props('loading')).toBe(true);
+    });
+  });
+
+  describe('When there are active runners', () => {
+    beforeEach(async () => {
+      mostActiveRunnersHandler.mockResolvedValue(mostActiveRunnersData);
+
+      createComponent();
+      await waitForPromises();
+    });
+
+    it('shows results', () => {
+      expect(findRunnersActiveList().props('loading')).toBe(false);
+      expect(findRunnersActiveList().props('activeRunners')).toHaveLength(2);
+    });
+
+    it('shows runner jobs url', () => {
+      const { adminUrl, ...runner } = mockRunner;
+      expect(findRunnersActiveList().props('activeRunners')[0]).toMatchObject(runner);
+      expect(findRunnersActiveList().props('activeRunners')[0].jobsUrl).toEqual(
+        `${adminUrl}#${JOBS_ROUTE_PATH}`,
+      );
+    });
+  });
+
+  describe('When there are active runners with no active jobs', () => {
+    beforeEach(async () => {
+      mostActiveRunnersHandler.mockResolvedValue({
+        data: {
+          runners: {
+            nodes: [
+              mockRunner,
+              {
+                ...mockRunner2,
+                runningJobCount: 0,
+              },
+            ],
+          },
+        },
+      });
+
+      createComponent();
+      await waitForPromises();
+    });
+
+    it('ignores runners with no active jobs', () => {
+      expect(findRunnersActiveList().props('activeRunners')).toHaveLength(1);
+      expect(findRunnersActiveList().props('activeRunners')[0].id).toBe(mockRunner.id);
+    });
+  });
+
+  describe('When an error occurs', () => {
+    beforeEach(async () => {
+      mostActiveRunnersHandler.mockRejectedValue(new Error('Error!'));
+
+      createComponent();
+      await waitForPromises();
+    });
+
+    it('shows an error', () => {
+      expect(createAlert).toHaveBeenCalled();
+    });
+
+    it('reports an error', () => {
+      expect(captureException).toHaveBeenCalledWith({
+        component: 'AdminRunnerActiveList',
+        error: expect.any(Error),
+      });
+    });
+  });
+});
diff --git a/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app_spec.js b/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app_spec.js
index fb1532039ec50e4bccda44642917895bb7c1b433..8e2de94c51ab3379a543a3870e7690c8384ddda4 100644
--- a/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app_spec.js
+++ b/ee/spec/frontend/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app_spec.js
@@ -1,13 +1,14 @@
 import { GlButton } from '@gitlab/ui';
 
 import AdminRunnersDashboardApp from 'ee/ci/runner/admin_runners_dashboard/admin_runners_dashboard_app.vue';
+import AdminRunnerActiveList from 'ee/ci/runner/admin_runners_dashboard/admin_runners_active_list.vue';
+
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 
 import RunnerDashboardStatOnline from 'ee/ci/runner/components/runner_dashboard_stat_online.vue';
 import RunnerDashboardStatOffline from 'ee/ci/runner/components/runner_dashboard_stat_offline.vue';
 import RunnerUsage from 'ee/ci/runner/components/runner_usage.vue';
 import RunnerJobFailures from 'ee/ci/runner/components/runner_job_failures.vue';
-import RunnerActiveList from 'ee/ci/runner/components/runner_active_list.vue';
 import RunnerWaitTimes from 'ee/ci/runner/components/runner_wait_times.vue';
 
 const mockAdminRunnersPath = '/runners/list';
@@ -43,7 +44,7 @@ describe('AdminRunnersDashboardApp', () => {
   it('shows dashboard panels', () => {
     expect(wrapper.findComponent(RunnerDashboardStatOnline).exists()).toBe(true);
     expect(wrapper.findComponent(RunnerDashboardStatOffline).exists()).toBe(true);
-    expect(wrapper.findComponent(RunnerActiveList).exists()).toBe(true);
+    expect(wrapper.findComponent(AdminRunnerActiveList).exists()).toBe(true);
     expect(wrapper.findComponent(RunnerWaitTimes).exists()).toBe(true);
   });
 
diff --git a/ee/spec/frontend/ci/runner/components/runner_active_list_spec.js b/ee/spec/frontend/ci/runner/components/runner_active_list_spec.js
index 69df9e5a1896788db792a0c69c272c9008ff25d0..e6ad9af52237c42a60c79b0bc8f8e30e2512835e 100644
--- a/ee/spec/frontend/ci/runner/components/runner_active_list_spec.js
+++ b/ee/spec/frontend/ci/runner/components/runner_active_list_spec.js
@@ -1,37 +1,24 @@
 import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import {
-  extendedWrapper,
-  shallowMountExtended,
-  mountExtended,
-} from 'helpers/vue_test_utils_helper';
-import { stubComponent } from 'helpers/stub_component';
+import { extendedWrapper, mountExtended } from 'helpers/vue_test_utils_helper';
 
 import RunnerActiveList from 'ee/ci/runner/components/runner_active_list.vue';
-import mostActiveRunnersQuery from 'ee/ci/runner/graphql/performance/most_active_runners.graphql';
-
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { s__ } from '~/locale';
 import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/alert';
-import { captureException } from '~/ci/runner/sentry_utils';
 import { JOBS_ROUTE_PATH } from '~/ci/runner/constants';
-
 import { mostActiveRunnersData } from '../mock_data';
 
 jest.mock('~/alert');
 jest.mock('~/ci/runner/sentry_utils');
 
 const mostActiveRunners = mostActiveRunnersData.data.runners.nodes;
-const [mockRunner, mockRunner2] = mostActiveRunners;
+const [{ adminUrl, ...mockRunner }, { adminUrl2, ...mockRunner2 }] = mostActiveRunners;
 
-Vue.use(VueApollo);
+mockRunner.jobsUrl = `${adminUrl}#${JOBS_ROUTE_PATH}`;
+mockRunner2.jobsUrl = `${adminUrl2}#${JOBS_ROUTE_PATH}`;
+
+// Vue.use(VueApollo);
 
 describe('RunnerActiveList', () => {
   let wrapper;
-  let mostActiveRunnersHandler;
 
   const findTable = () => wrapper.findComponent(GlTable);
   const findHeaders = () => wrapper.findAll('thead th');
@@ -39,41 +26,31 @@ describe('RunnerActiveList', () => {
   const findCell = (row = 0, fieldKey) =>
     extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
 
-  const createComponent = ({ mountFn = shallowMountExtended, ...options } = {}) => {
-    wrapper = mountFn(RunnerActiveList, {
-      apolloProvider: createMockApollo([[mostActiveRunnersQuery, mostActiveRunnersHandler]]),
+  const createComponent = ({ props = {}, ...options } = {}) => {
+    wrapper = mountExtended(RunnerActiveList, {
+      propsData: {
+        ...props,
+      },
       ...options,
     });
   };
 
-  beforeEach(() => {
-    mostActiveRunnersHandler = jest.fn();
-  });
-
-  it('Requests most active runners', () => {
-    createComponent({
-      stubs: {
-        GlTable: stubComponent(GlTable),
-      },
-    });
-
-    expect(mostActiveRunnersHandler).toHaveBeenCalledTimes(1);
-  });
-
   describe('When loading data', () => {
     it('should show a loading skeleton', () => {
-      createComponent({ mountFn: mountExtended });
+      createComponent({ props: { loading: true }, mountFn: mountExtended });
 
       expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
     });
   });
 
   describe('When there are active runners', () => {
-    beforeEach(async () => {
-      mostActiveRunnersHandler.mockResolvedValue(mostActiveRunnersData);
-
-      createComponent({ mountFn: mountExtended });
-      await waitForPromises();
+    beforeEach(() => {
+      createComponent({
+        props: {
+          activeRunners: [mockRunner, mockRunner2],
+        },
+        mountFn: mountExtended,
+      });
     });
 
     it('shows table', () => {
@@ -82,7 +59,7 @@ describe('RunnerActiveList', () => {
 
     it('shows headers', () => {
       const headers = findHeaders().wrappers.map((w) => w.text());
-      expect(headers).toEqual(['', s__('Runners|Runner'), s__('Runners|Running Jobs')]);
+      expect(headers).toEqual(['', 'Runner', 'Running Jobs']);
     });
 
     it('shows runners', () => {
@@ -105,60 +82,15 @@ describe('RunnerActiveList', () => {
       expect(findCell(1, 'runningJobCount').text()).toBe('1');
     });
 
-    it('shows jobs link', async () => {
-      createComponent({ mountFn: mountExtended });
-      await waitForPromises();
-
+    it('shows jobs link', () => {
       const url = findCell(0, 'runningJobCount').findComponent(GlLink).attributes('href');
-      expect(url).toBe(`${mockRunner.adminUrl}#${JOBS_ROUTE_PATH}`);
-    });
-  });
-
-  describe('When there are active runners with no active jobs', () => {
-    beforeEach(async () => {
-      mostActiveRunnersHandler.mockResolvedValue({
-        data: {
-          runners: {
-            nodes: [
-              mockRunner,
-              {
-                ...mockRunner2,
-                runningJobCount: 0,
-              },
-            ],
-          },
-        },
-      });
-
-      createComponent({ mountFn: mountExtended });
-      await waitForPromises();
-    });
-
-    it('ignores runners with no active jobs', () => {
-      expect(findRows()).toHaveLength(1);
-
-      // Row 1
-      const runner = `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha}) - ${
-        mockRunner.description
-      }`;
-      expect(findCell(0, 'index').text()).toBe('1');
-      expect(findCell(0, 'runner').text()).toBe(runner);
-      expect(findCell(0, 'runningJobCount').text()).toBe('2');
+      expect(url).toBe(mockRunner.jobsUrl);
     });
   });
 
   describe('When there are no runners', () => {
-    beforeEach(async () => {
-      mostActiveRunnersHandler.mockResolvedValueOnce({
-        data: {
-          runners: {
-            nodes: [],
-          },
-        },
-      });
-
+    beforeEach(() => {
       createComponent({ mountFn: mountExtended });
-      await waitForPromises();
     });
 
     it('should render no runners', () => {
@@ -167,28 +99,4 @@ describe('RunnerActiveList', () => {
       expect(wrapper.text()).toContain('no runners');
     });
   });
-
-  describe('When an error occurs', () => {
-    beforeEach(async () => {
-      mostActiveRunnersHandler.mockRejectedValue(new Error('Error!'));
-
-      createComponent({
-        stubs: {
-          GlTable: stubComponent(GlTable),
-        },
-      });
-      await waitForPromises();
-    });
-
-    it('shows an error', () => {
-      expect(createAlert).toHaveBeenCalled();
-    });
-
-    it('reports an error', () => {
-      expect(captureException).toHaveBeenCalledWith({
-        component: 'RunnerActiveList',
-        error: expect.any(Error),
-      });
-    });
-  });
 });
diff --git a/ee/spec/frontend/ci/runner/mock_data.js b/ee/spec/frontend/ci/runner/mock_data.js
index e9af32b92ccbc90a356306fe26c727d0968930b6..40518e13f549baff37908e17d275059326944650 100644
--- a/ee/spec/frontend/ci/runner/mock_data.js
+++ b/ee/spec/frontend/ci/runner/mock_data.js
@@ -4,7 +4,7 @@
 import allRunnersUpgradeStatusData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.upgrade_status.json';
 
 // Dashboard queries
-import mostActiveRunnersData from 'test_fixtures/graphql/ci/runner/performance/most_active_runners.graphql.json';
+import mostActiveRunnersData from 'test_fixtures/graphql/ci/runner/performance/most_active_runners.query.graphql.json';
 import runnerFailedJobsData from 'test_fixtures/graphql/ci/runner/performance/runner_failed_jobs.graphql.json';
 
 export const runnersWaitTimes = {
diff --git a/ee/spec/frontend/fixtures/runner.rb b/ee/spec/frontend/fixtures/runner.rb
index 5894c61fdeaebfcbfc70f28487c7e6671534a67f..7ebeb2596f8ff80676914c89d76cda4634497261 100644
--- a/ee/spec/frontend/fixtures/runner.rb
+++ b/ee/spec/frontend/fixtures/runner.rb
@@ -50,7 +50,7 @@
     end
 
     describe 'most_active_runners.query.graphql', type: :request do
-      runner_jobs_query = 'performance/most_active_runners.graphql'
+      runner_jobs_query = 'performance/most_active_runners.query.graphql'
       let_it_be(:query) do
         get_graphql_query_as_string("#{query_path}#{runner_jobs_query}", ee: true)
       end