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