From aa779d93e3bdfa1bec64ec0c10c6a6b770bde9b6 Mon Sep 17 00:00:00 2001
From: Justin Ho Tuan Duong <hduong@gitlab.com>
Date: Wed, 7 Feb 2024 08:21:16 +0000
Subject: [PATCH] Add import stats to bulk_import_history_app

Changelog: changed
---
 app/assets/javascripts/import/constants.js    | 10 ++
 .../components/import_stats.vue               | 78 +++++++++++++++
 .../components/bulk_imports_history_app.vue   | 24 ++++-
 locale/gitlab.pot                             | 12 +++
 .../components/import_stats_spec.js           | 96 +++++++++++++++++++
 .../bulk_imports_history_app_spec.js          | 23 ++++-
 6 files changed, 236 insertions(+), 7 deletions(-)
 create mode 100644 app/assets/javascripts/import_entities/components/import_stats.vue
 create mode 100644 spec/frontend/import_entities/components/import_stats_spec.js

diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index c7917334ffd04..416585c228dd2 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -14,6 +14,16 @@ export const BULK_IMPORT_STATIC_ITEMS = {
   milestones: __('Milestone'),
   namespace_settings: s__('GroupSettings|Namespace setting'),
   project: __('Project'),
+  self: __('Group'),
+  auto_devops: __('Auto DevOps'),
+  ci_pipelines: __('CI pipeline'),
+  container_expiration_policy: __('Container expiration policy'),
+  design: __('Design'),
+  project_feature: __('Project feature'),
+  protected_branches: __('Protected Branch'),
+  push_rule: __('Push Rule'),
+  repository: __('Repository'),
+  service_desk_setting: __('Service Desk'),
 };
 
 const STATISTIC_ITEMS = {
diff --git a/app/assets/javascripts/import_entities/components/import_stats.vue b/app/assets/javascripts/import_entities/components/import_stats.vue
new file mode 100644
index 0000000000000..589bd30f1e94d
--- /dev/null
+++ b/app/assets/javascripts/import_entities/components/import_stats.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlAccordion, GlAccordionItem, GlIcon } from '@gitlab/ui';
+
+import { BULK_IMPORT_STATIC_ITEMS } from '~/import/constants';
+import { STATUSES } from '../constants';
+
+export default {
+  name: 'ImportStats',
+
+  components: {
+    GlAccordion,
+    GlAccordionItem,
+    GlIcon,
+  },
+
+  props: {
+    stats: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    statsMapping: {
+      type: Object,
+      required: false,
+      default: () => BULK_IMPORT_STATIC_ITEMS,
+    },
+    status: {
+      type: String,
+      required: true,
+    },
+  },
+
+  methods: {
+    importedByType(type) {
+      return this.stats[type].imported || 0;
+    },
+
+    fetchedByType(type) {
+      return this.stats[type].fetched;
+    },
+
+    statsIconProps(type) {
+      const fetched = this.fetchedByType(type);
+      const imported = this.importedByType(type);
+
+      if (fetched === imported) {
+        if (imported === 0) {
+          return { name: 'status-scheduled', class: 'gl-text-gray-400' };
+        }
+
+        return { name: 'status-success', class: 'gl-text-green-400' };
+      }
+
+      if (this.status === STATUSES.FINISHED) {
+        return { name: 'status-alert', class: 'gl-text-orange-400' };
+      }
+
+      return { name: 'status-running', class: 'gl-text-blue-400' };
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-accordion :header-level="3">
+    <gl-accordion-item :title="__('Details')">
+      <ul class="gl-p-0 gl-mb-3 gl-list-style-none gl-font-sm">
+        <li v-for="key in Object.keys(stats)" :key="key" data-testid="import-stat-item">
+          <div class="gl-display-flex gl-w-28 gl-align-items-center">
+            <gl-icon :size="12" class="gl-mr-2 gl-flex-shrink-0" v-bind="statsIconProps(key)" />
+            <span>{{ statsMapping[key] || key }}</span>
+            <span class="gl-ml-auto"> {{ importedByType(key) }}/{{ fetchedByType(key) }} </span>
+          </div>
+        </li>
+      </ul>
+    </gl-accordion-item>
+  </gl-accordion>
+</template>
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index e66040c5a9938..c65a9fb9116c3 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -7,13 +7,15 @@ import {
   GlTableLite,
   GlTooltipDirective as GlTooltip,
 } from '@gitlab/ui';
-import { isEqual } from 'lodash';
+import { isEmpty, isEqual } from 'lodash';
 
 import { s__, __ } from '~/locale';
 import { createAlert } from '~/alert';
 import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
 import { joinPaths, getParameterValues } from '~/lib/utils/url_utility';
 import { getBulkImportHistory, getBulkImportsHistory } from '~/rest_api';
+import { BULK_IMPORT_STATIC_ITEMS } from '~/import/constants';
+import ImportStats from '~/import_entities/components/import_stats.vue';
 import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
 import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
 
@@ -47,6 +49,7 @@ export default {
     GlLoadingIcon,
     GlTableLite,
     PaginationBar,
+    ImportStats,
     ImportStatus,
     TimeAgo,
     LocalStorageSync,
@@ -79,7 +82,7 @@ export default {
     tableCell({
       key: 'destination_name',
       label: s__('BulkImport|Destination'),
-      thClass: `gl-w-40p`,
+      thClass: `gl-w-30p`,
     }),
     tableCell({
       key: 'created_at',
@@ -88,6 +91,7 @@ export default {
     tableCell({
       key: 'status',
       label: __('Status'),
+      thClass: `gl-w-quarter`,
     }),
   ],
 
@@ -199,6 +203,10 @@ export default {
       return this.pathWithSuffix(fullPath, item);
     },
 
+    hasStats(item) {
+      return !isEmpty(item.stats);
+    },
+
     getEntityTooltip(item) {
       switch (item.entity_type) {
         case WORKSPACE_PROJECT:
@@ -218,6 +226,7 @@ export default {
 
   gitlabLogo: window.gon.gitlab_logo,
   historyPaginationSizePersistKey: HISTORY_PAGINATION_SIZE_PERSIST_KEY,
+  BULK_IMPORT_STATIC_ITEMS,
 };
 </script>
 
@@ -257,15 +266,20 @@ export default {
           <time-ago :time="value" />
         </template>
         <template #cell(status)="{ value, item }">
-          <div
-            class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-flex-start gl-justify-content-space-between gl-gap-3"
-          >
+          <div>
             <import-status
               :id="item.bulk_import_id"
               :entity-id="item.id"
               :has-failures="item.has_failures"
               :status="value"
             />
+            <import-stats
+              v-if="hasStats(item)"
+              :stats="item.stats"
+              :stats-mapping="$options.BULK_IMPORT_STATIC_ITEMS"
+              :status="value"
+              class="gl-mt-2"
+            />
           </div>
         </template>
       </gl-table-lite>
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f05ee97e45d98..3da020b72c20c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9378,6 +9378,9 @@ msgstr ""
 msgid "CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}"
 msgstr ""
 
+msgid "CI pipeline"
+msgstr ""
+
 msgid "CI settings"
 msgstr ""
 
@@ -13379,6 +13382,9 @@ msgstr ""
 msgid "Container Scanning"
 msgstr ""
 
+msgid "Container expiration policy"
+msgstr ""
+
 msgid "Container must be a group."
 msgstr ""
 
@@ -38288,6 +38294,9 @@ msgstr ""
 msgid "Project export started. A download link will be sent by email and made available on this page."
 msgstr ""
 
+msgid "Project feature"
+msgstr ""
+
 msgid "Project groups"
 msgstr ""
 
@@ -40253,6 +40262,9 @@ msgstr ""
 msgid "Push"
 msgstr ""
 
+msgid "Push Rule"
+msgstr ""
+
 msgid "Push Rule updated successfully."
 msgstr ""
 
diff --git a/spec/frontend/import_entities/components/import_stats_spec.js b/spec/frontend/import_entities/components/import_stats_spec.js
new file mode 100644
index 0000000000000..18912a42e48ad
--- /dev/null
+++ b/spec/frontend/import_entities/components/import_stats_spec.js
@@ -0,0 +1,96 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ImportStats from '~/import_entities/components/import_stats.vue';
+import { STATUSES } from '~/import_entities/constants';
+
+describe('Import entities stats component', () => {
+  let wrapper;
+
+  const mockStats = {
+    labels: {
+      fetched: 10,
+      imported: 9,
+    },
+    self: {
+      fetched: 1,
+      imported: 1,
+    },
+    milestones: {
+      fetched: 1000,
+      imported: 999,
+    },
+    namespace_settings: {
+      fetched: 0,
+      imported: 0,
+    },
+  };
+
+  const defaultProps = {
+    stats: mockStats,
+    status: STATUSES.CREATED,
+  };
+
+  const createComponent = ({ props } = {}) => {
+    wrapper = shallowMountExtended(ImportStats, {
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+    });
+  };
+
+  const findAllStatItems = () => wrapper.findAllByTestId('import-stat-item');
+  const findGlIcon = (item) => item.findComponent(GlIcon);
+
+  describe('template', () => {
+    it('renders items', () => {
+      const expectedText = [
+        {
+          item: 'Label',
+          stat: '9/10',
+        },
+        {
+          item: 'Group',
+          stat: '1/1',
+        },
+        {
+          item: 'Milestone',
+          stat: '999/1000',
+        },
+        {
+          item: 'Namespace setting',
+          stat: '0/0',
+        },
+      ];
+
+      createComponent();
+
+      const items = findAllStatItems();
+      expect(items).toHaveLength(Object.keys(mockStats).length);
+
+      items.wrappers.forEach((item, index) => {
+        expect(item.text()).toContain(expectedText[index].item);
+        expect(item.text()).toContain(expectedText[index].stat);
+      });
+    });
+
+    describe.each`
+      status               | expectedIcons
+      ${STATUSES.CREATED}  | ${['running', 'success', 'running', 'scheduled']}
+      ${STATUSES.FINISHED} | ${['alert', 'success', 'alert', 'scheduled']}
+    `('when status is $status', ({ status, expectedIcons }) => {
+      it('renders correct item icons', () => {
+        createComponent({
+          props: {
+            status,
+          },
+        });
+
+        findAllStatItems().wrappers.forEach((item, index) => {
+          expect(findGlIcon(item).props().name).toBe(`status-${expectedIcons[index]}`);
+        });
+      });
+    });
+  });
+});
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index 1c9d8f17210be..5f3087e253d8e 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -8,6 +8,7 @@ import { getParameterValues } from '~/lib/utils/url_utility';
 
 import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
 import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
+import ImportStats from '~/import_entities/components/import_stats.vue';
 import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
 import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
 
@@ -41,6 +42,12 @@ describe('BulkImportsHistoryApp', () => {
       created_at: '2021-07-08T10:03:44.743Z',
       has_failures: false,
       failures: [],
+      stats: {
+        labels: {
+          fetched: 10,
+          imported: 9,
+        },
+      },
     },
     {
       id: 2,
@@ -67,6 +74,7 @@ describe('BulkImportsHistoryApp', () => {
           created_at: '2021-07-13T13:34:49.344Z',
         },
       ],
+      stats: {},
     },
   ];
 
@@ -86,6 +94,7 @@ describe('BulkImportsHistoryApp', () => {
 
   const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
   const findPaginationBar = () => wrapper.findComponent(PaginationBar);
+  const findTableRow = (index) => wrapper.findAll('tbody tr').at(index);
   const findImportStatusAt = (index) => wrapper.findAllComponents(ImportStatus).at(index);
 
   beforeEach(() => {
@@ -261,6 +270,16 @@ describe('BulkImportsHistoryApp', () => {
       expect(failedImportStatusLink.text()).toBe('See failures');
       expect(failedImportStatusLink.attributes('href')).toContain('/mock-details');
     });
+
+    it('renders import stats', () => {
+      expect(findTableRow(0).findComponent(ImportStats).props('stats')).toEqual(
+        DUMMY_RESPONSE[0].stats,
+      );
+    });
+
+    it('does not render import stats when not available', () => {
+      expect(findTableRow(1).findComponent(ImportStats).exists()).toBe(false);
+    });
   });
 
   describe('status polling', () => {
@@ -311,7 +330,7 @@ describe('BulkImportsHistoryApp', () => {
           BULK_IMPORTS_API_URL,
           mockRealtimeChangesPath,
         ]);
-        expect(wrapper.findAll('tbody tr').at(0).text()).toContain('Pending');
+        expect(findTableRow(0).text()).toContain('Pending');
       });
 
       it('stops polling when import is finished', async () => {
@@ -326,7 +345,7 @@ describe('BulkImportsHistoryApp', () => {
           mockRealtimeChangesPath,
           mockRealtimeChangesPath,
         ]);
-        expect(wrapper.findAll('tbody tr').at(0).text()).toContain('Complete');
+        expect(findTableRow(0).text()).toContain('Complete');
       });
     });
   });
-- 
GitLab