diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue index cc6a057f58758d0cbf01430a0b49adddc1909bd4..9262a4e1e95705801944dc7a9136edb96e887dbd 100644 --- a/app/assets/javascripts/import_entities/components/import_status.vue +++ b/app/assets/javascripts/import_entities/components/import_status.vue @@ -1,10 +1,66 @@ diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js index 1ab5413a5cc2fe101defaec626950bdfbff60aa4..20a4d2d84b4db9f6d6a019cdcc12b2048783cd8d 100644 --- a/app/assets/javascripts/import_entities/constants.js +++ b/app/assets/javascripts/import_entities/constants.js @@ -1,5 +1,3 @@ -import { __ } from '~/locale'; - // The `scheduling` status is only present on the client-side, // it is used as the status when we are requesting to start an import. @@ -13,42 +11,3 @@ export const STATUSES = { SCHEDULING: 'scheduling', CANCELLED: 'cancelled', }; - -const SCHEDULED_STATUS = { - icon: 'status-scheduled', - text: __('Pending'), - iconClass: 'gl-text-orange-400', -}; - -const STATUS_MAP = { - [STATUSES.NONE]: { - icon: 'status-waiting', - text: __('Not started'), - iconClass: 'gl-text-gray-400', - }, - [STATUSES.SCHEDULING]: SCHEDULED_STATUS, - [STATUSES.SCHEDULED]: SCHEDULED_STATUS, - [STATUSES.CREATED]: SCHEDULED_STATUS, - [STATUSES.STARTED]: { - icon: 'status-running', - text: __('Importing...'), - iconClass: 'gl-text-blue-400', - }, - [STATUSES.FINISHED]: { - icon: 'status-success', - text: __('Complete'), - iconClass: 'gl-text-green-400', - }, - [STATUSES.FAILED]: { - icon: 'status-failed', - text: __('Failed'), - iconClass: 'gl-text-red-600', - }, - [STATUSES.CANCELLED]: { - icon: 'status-stopped', - text: __('Cancelled'), - iconClass: 'gl-text-red-600', - }, -}; - -export default STATUS_MAP; diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue index c3d0ca4ed8ca9be32e8ab7f2d2edef687d12974c..e4090a378e1c5b556303b4c7978e2922c86c0eca 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue @@ -69,6 +69,10 @@ export default { return getImportStatus(this.repo); }, + stats() { + return this.repo.importedProject?.stats; + }, + importTarget() { return this.getImportTarget(this.repo.importSource.id); }, @@ -101,11 +105,11 @@ export default { - - + + - + { const repo = state.repositories.find((p) => p.importedProject?.id === updatedProject.id); if (repo?.importedProject) { - repo.importedProject.importStatus = updatedProject.importStatus; + repo.importedProject = { + ...repo.importedProject, + stats: updatedProject.stats, + importStatus: updatedProject.importStatus, + }; } }); }, diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss index b7a4d9564fe155e24c195de51671076e22cacaff..cd5e6d32e4e72de473a386738f2ca14d91954aec 100644 --- a/app/assets/stylesheets/page_bundles/import.scss +++ b/app/assets/stylesheets/page_bundles/import.scss @@ -1,18 +1,22 @@ @import 'mixins_and_variables_and_functions'; +.import-jobs-from-col { + width: 37%; +} + + .import-jobs-to-col { - width: 39%; + width: 37%; } .import-jobs-status-col { - width: 15%; + width: 25%; } .import-jobs-cta-col { width: 1%; } - .import-entities-target-select { &.disabled { .import-entities-target-select-separator { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 632169b328941b751dcef14c9f4719396437bfaa..1c7fe5e3ba11a13480271b9204748c8cdd7fc6ee 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13016,6 +13016,9 @@ msgstr "" msgid "Diff limits" msgstr "" +msgid "Diff notes" +msgstr "" + msgid "Difference between start date and now" msgstr "" @@ -17085,6 +17088,15 @@ msgstr "" msgid "Gitea Import" msgstr "" +msgid "GithubImporter|PR mergers" +msgstr "" + +msgid "GithubImporter|PR reviews" +msgstr "" + +msgid "GithubImporter|Pull requests" +msgstr "" + msgid "GithubIntegration|Create a %{token_link_start}personal access token%{token_link_end} with %{status_html} access granted and paste it here." msgstr "" @@ -25642,6 +25654,9 @@ msgstr "" msgid "NoteForm|Note" msgstr "" +msgid "Notes" +msgstr "" + msgid "Notes rate limit" msgstr "" @@ -27010,6 +27025,9 @@ msgstr "" msgid "Part of merge request changes" msgstr "" +msgid "Partial import" +msgstr "" + msgid "Participants" msgstr "" diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..686a21e3923bfb6f9a4a3c6814fe16f68c1c7384 --- /dev/null +++ b/spec/frontend/import_entities/components/import_status_spec.js @@ -0,0 +1,145 @@ +import { GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import ImportStatus from '~/import_entities/components/import_status.vue'; +import { STATUSES } from '~/import_entities/constants'; + +describe('Import entities status component', () => { + let wrapper; + + const createComponent = (propsData) => { + wrapper = shallowMount(ImportStatus, { + propsData, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('success status', () => { + const getStatusText = () => wrapper.findComponent(GlBadge).text(); + + it('displays finished status as complete when no stats are provided', () => { + createComponent({ + status: STATUSES.FINISHED, + }); + expect(getStatusText()).toBe('Complete'); + }); + + it('displays finished status as complete when all stats items were processed', () => { + const statItems = { label: 100, note: 200 }; + + createComponent({ + status: STATUSES.FINISHED, + stats: { + fetched: { ...statItems }, + imported: { ...statItems }, + }, + }); + + expect(getStatusText()).toBe('Complete'); + }); + + it('displays finished status as partial when all stats items were processed', () => { + const statItems = { label: 100, note: 200 }; + + createComponent({ + status: STATUSES.FINISHED, + stats: { + fetched: { ...statItems }, + imported: { ...statItems, label: 50 }, + }, + }); + + expect(getStatusText()).toBe('Partial import'); + }); + }); + + describe('details drawer', () => { + const findDetailsDrawer = () => wrapper.findComponent(GlAccordionItem); + + it('renders details drawer to be present when stats are provided', () => { + createComponent({ + status: 'created', + stats: { fetched: { label: 1 }, imported: { label: 0 } }, + }); + + expect(findDetailsDrawer().exists()).toBe(true); + }); + + it('does not render details drawer when no stats are provided', () => { + createComponent({ + status: 'created', + }); + + expect(findDetailsDrawer().exists()).toBe(false); + }); + + it('does not render details drawer when stats are empty', () => { + createComponent({ + status: 'created', + stats: { fetched: {}, imported: {} }, + }); + + expect(findDetailsDrawer().exists()).toBe(false); + }); + + it('does not render details drawer when no known stats are provided', () => { + createComponent({ + status: 'created', + stats: { + fetched: { + UNKNOWN_STAT: 100, + }, + imported: { + UNKNOWN_STAT: 0, + }, + }, + }); + + expect(findDetailsDrawer().exists()).toBe(false); + }); + }); + + describe('stats display', () => { + const getStatusIcon = () => + wrapper.findComponent(GlAccordionItem).findComponent(GlIcon).props().name; + + const createComponentWithStats = ({ fetched, imported }) => { + createComponent({ + status: 'created', + stats: { + fetched: { label: fetched }, + imported: { label: imported }, + }, + }); + }; + + it('displays scheduled status when imported is 0', () => { + createComponentWithStats({ + fetched: 100, + imported: 0, + }); + + expect(getStatusIcon()).toBe('status-scheduled'); + }); + + it('displays running status when imported is not equal to fetched', () => { + createComponentWithStats({ + fetched: 100, + imported: 10, + }); + + expect(getStatusIcon()).toBe('status-running'); + }); + + it('displays success status when imported is equal to fetched', () => { + createComponentWithStats({ + fetched: 100, + imported: 100, + }); + + expect(getStatusIcon()).toBe('status-success'); + }); + }); +}); diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index c8afa9ea57d9a44980edfad1d789adb891d5b2d0..41a005199e12fe027e1aa2cf88cefc572c940498 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -98,6 +98,8 @@ describe('ProviderRepoTableRow', () => { }); describe('when rendering imported project', () => { + const FAKE_STATS = {}; + const repo = { importSource: { id: 'remote-1', @@ -109,6 +111,7 @@ describe('ProviderRepoTableRow', () => { fullPath: 'fullPath', importSource: 'importSource', importStatus: STATUSES.FINISHED, + stats: FAKE_STATS, }, }; @@ -134,6 +137,10 @@ describe('ProviderRepoTableRow', () => { it('does not render import button', () => { expect(findImportButton().exists()).toBe(false); }); + + it('passes stats to import status component', () => { + expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS); + }); }); describe('when rendering incompatible project', () => { diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js index e062d889325c915a02793d0a3a70aa602abf2c2e..77fae951300e094ea8ec94e8eddafa2691b769e5 100644 --- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js @@ -232,6 +232,35 @@ describe('import_projects store mutations', () => { updatedProjects[0].importStatus, ); }); + + it('updates import stats of project', () => { + const repoId = 1; + state = { + repositories: [ + { importedProject: { id: repoId, stats: {} }, importStatus: STATUSES.STARTED }, + ], + }; + const newStats = { + fetched: { + label: 10, + }, + imported: { + label: 1, + }, + }; + + const updatedProjects = [ + { + id: repoId, + importStatus: STATUSES.FINISHED, + stats: newStats, + }, + ]; + + mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); + + expect(state.repositories[0].importedProject.stats).toStrictEqual(newStats); + }); }); describe(`${types.REQUEST_NAMESPACES}`, () => {