From b620584b51a1e72064cb4e0715deb976d1432e27 Mon Sep 17 00:00:00 2001
From: Miguel Rincon <mrincon@gitlab.com>
Date: Thu, 8 Feb 2024 10:17:00 +0100
Subject: [PATCH] Handle row for "others" in runner usage breakdown

In some cases, the results for top runners and top projects may render a
last result that provides a total summary of the remainder projects
and runners. This change handles that case.

Changelog: fixed
EE: true
---
 .../ci/runner/components/runner_usage.vue     | 24 ++++++----
 .../ci/runner/components/runner_usage_spec.js | 44 +++++++++++++------
 locale/gitlab.pot                             |  6 +++
 3 files changed, 51 insertions(+), 23 deletions(-)

diff --git a/ee/app/assets/javascripts/ci/runner/components/runner_usage.vue b/ee/app/assets/javascripts/ci/runner/components/runner_usage.vue
index ac619cbf4477a..1d3cb414a6de5 100644
--- a/ee/app/assets/javascripts/ci/runner/components/runner_usage.vue
+++ b/ee/app/assets/javascripts/ci/runner/components/runner_usage.vue
@@ -155,14 +155,17 @@ export default {
         data-testid="top-projects-table"
       >
         <template #cell(project)="{ value }">
-          <gl-avatar
-            :label="value.name"
-            :src="value.avatarUrl"
-            shape="rect"
-            :size="16"
-            :entity-name="value.name"
-          />
-          <gl-link :href="value.webUrl" class="gl-text-body!">{{ value.name }}</gl-link>
+          <template v-if="value">
+            <gl-avatar
+              :label="value.name"
+              :src="value.avatarUrl"
+              shape="rect"
+              :size="16"
+              :entity-name="value.name"
+            />
+            <gl-link :href="value.webUrl" class="gl-text-body!"> {{ value.name }} </gl-link>
+          </template>
+          <template v-else> {{ s__('Runners|Other projects') }} </template>
         </template>
 
         <template #cell(ciMinutesUsed)="{ value }">{{ formatNumber(value) }}</template>
@@ -175,7 +178,10 @@ export default {
         data-testid="top-runners-table"
       >
         <template #cell(runner)="{ value }">
-          <gl-link :href="value.adminUrl" class="gl-text-body!">{{ runnerName(value) }}</gl-link>
+          <gl-link v-if="value" :href="value.adminUrl" class="gl-text-body!">
+            {{ runnerName(value) }}
+          </gl-link>
+          <template v-else> {{ s__('Runners|Other runners') }} </template>
         </template>
         <template #cell(ciMinutesUsed)="{ value }">{{ formatNumber(value) }}</template>
       </gl-table-lite>
diff --git a/ee/spec/frontend/ci/runner/components/runner_usage_spec.js b/ee/spec/frontend/ci/runner/components/runner_usage_spec.js
index 8e012c2698979..9f15573444de0 100644
--- a/ee/spec/frontend/ci/runner/components/runner_usage_spec.js
+++ b/ee/spec/frontend/ci/runner/components/runner_usage_spec.js
@@ -31,7 +31,7 @@ const mockRunnerUsage = [
       adminUrl: '/admin/runners/1',
       __typename: 'CiRunner',
     },
-    ciMinutesUsed: 1001,
+    ciMinutesUsed: 2002,
     __typename: 'CiRunnerUsage',
   },
   {
@@ -42,7 +42,12 @@ const mockRunnerUsage = [
       adminUrl: '/admin/runners/2',
       __typename: 'CiRunner',
     },
-    ciMinutesUsed: 11,
+    ciMinutesUsed: 2001,
+    __typename: 'CiRunnerUsage',
+  },
+  {
+    runner: null,
+    ciMinutesUsed: 2000,
     __typename: 'CiRunnerUsage',
   },
 ];
@@ -67,7 +72,12 @@ const mockRunnerUsageByProject = [
       webUrl: '/group1/project2',
       __typename: 'Project',
     },
-    ciMinutesUsed: 12,
+    ciMinutesUsed: 1001,
+    __typename: 'CiRunnerUsageByProject',
+  },
+  {
+    project: null,
+    ciMinutesUsed: 1000,
     __typename: 'CiRunnerUsageByProject',
   },
 ];
@@ -127,10 +137,11 @@ describe('RunnerUsage', () => {
 
   it('loads top projects', async () => {
     createWrapper({ mountFn: mountExtended });
-
     await waitForPromises();
 
-    const [header, row1, row2] = findTopProjects().wrappers;
+    expect(findTopRunners().length).toBe(4);
+
+    const [header, row1, row2, row3] = findTopProjects().wrappers;
 
     expect(header.text()).toContain('Top projects consuming runners');
     expect(header.text()).toContain('Usage (min)');
@@ -147,26 +158,31 @@ describe('RunnerUsage', () => {
       src: '/project2.png',
     });
     expect(row2.text()).toContain('Project2');
-    expect(row2.text()).toContain('12');
+    expect(row2.text()).toContain('1,001');
+
+    expect(row3.text()).toContain('Other projects');
+    expect(row3.text()).toContain('1,000');
   });
 
   it('loads top runners', async () => {
     createWrapper({ mountFn: mountExtended });
-
     await waitForPromises();
 
-    const [header, row1, row2] = findTopRunners().wrappers.map((w) => w.text());
+    expect(findTopRunners().length).toBe(4);
+
+    const [header, row1, row2, row3] = findTopRunners().wrappers.map((w) => w.text());
 
     expect(header).toContain('Most used instance runners');
     expect(header).toContain('Usage (min)');
 
-    expect(row1).toContain('#1 (sha1)');
-    expect(row1).toContain('Runner 1');
-    expect(row1).toContain('1,001');
+    expect(row1).toContain('#1 (sha1) - Runner 1');
+    expect(row1).toContain('2,002');
+
+    expect(row2).toContain('#2 (sha2) - Runner 2');
+    expect(row2).toContain('2,001');
 
-    expect(row2).toContain('#2 (sha2)');
-    expect(row2).toContain('Runner 2');
-    expect(row2).toContain('11');
+    expect(row3).toContain('Other runners');
+    expect(row3).toContain('2,000');
   });
 
   it('calls mutation on button click', async () => {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f16c014d82431..ba89e55e8cc2f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -42675,6 +42675,12 @@ msgstr ""
 msgid "Runners|Operating systems"
 msgstr ""
 
+msgid "Runners|Other projects"
+msgstr ""
+
+msgid "Runners|Other runners"
+msgstr ""
+
 msgid "Runners|Owner"
 msgstr ""
 
-- 
GitLab