diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue index 73a255f392befa57bdd71dd3495a20d65db004f7..747d94d92f2f147183af1dad32448093303fcec7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue @@ -16,6 +16,9 @@ import { TRACKING_CATEGORIES } from '../../constants'; export const i18n = { downloadArtifacts: __('Download artifacts'), artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'), + artifactsFetchWarningMessage: s__( + 'Pipelines|Failed to update. Please reload page to update the list of artifacts.', + ), emptyArtifactsMessage: __('No artifacts found'), }; @@ -52,6 +55,7 @@ export default { hasError: false, isLoading: false, searchQuery: '', + isNewPipeline: false, }; }, computed: { @@ -64,13 +68,24 @@ export default { : this.artifacts; }, }, + watch: { + pipelineId() { + this.isNewPipeline = true; + }, + }, methods: { fetchArtifacts() { // refactor tracking based on action once this dropdown supports // actions other than artifacts this.track('click_artifacts_dropdown', { label: TRACKING_CATEGORIES.table }); + // Preserve the last good list and present it if a request fails + const oldArtifacts = [...this.artifacts]; + this.artifacts = []; + + this.hasError = false; this.isLoading = true; + // Replace the placeholder with the ID of the pipeline we are viewing const endpoint = this.artifactsEndpoint.replace( this.artifactsEndpointPlaceholder, @@ -80,9 +95,13 @@ export default { .get(endpoint) .then(({ data }) => { this.artifacts = data.artifacts; + this.isNewPipeline = false; }) .catch(() => { this.hasError = true; + if (!this.isNewPipeline) { + this.artifacts = oldArtifacts; + } }) .finally(() => { this.isLoading = false; @@ -108,10 +127,10 @@ export default { right lazy text-sr-only - @show.once="fetchArtifacts" + @show="fetchArtifacts" @shown="handleDropdownShown" > - <gl-alert v-if="hasError" variant="danger" :dismissible="false"> + <gl-alert v-if="hasError && !hasArtifacts" variant="danger" :dismissible="false"> {{ $options.i18n.artifactsFetchErrorMessage }} </gl-alert> @@ -136,5 +155,18 @@ export default { > {{ artifact.name }} </gl-dropdown-item> + + <template #footer> + <gl-dropdown-item + v-if="hasError && hasArtifacts" + class="gl-list-style-none" + disabled + data-testid="artifacts-fetch-warning" + > + <span class="gl-font-sm"> + {{ $options.i18n.artifactsFetchWarningMessage }} + </span> + </gl-dropdown-item> + </template> </gl-dropdown> </template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8a5a9b38db10bca6c5a82986bbc53d992a554944..f5df003e171045aee580c9a622d30d46a35e814f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34399,6 +34399,9 @@ msgstr "" msgid "Pipelines|Editor" msgstr "" +msgid "Pipelines|Failed to update. Please reload page to update the list of artifacts." +msgstr "" + msgid "Pipelines|Follow these instructions to install GitLab Runner on macOS." msgstr "" diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index 43336bbc7484d93c7d57e22c3fe05df2b0440bc9..0fdc45a5931cce4f9d09cba7e19ba2e682805ad3 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -28,6 +28,20 @@ describe('Pipeline Multi Actions Dropdown', () => { path: '/download/path-two', }, ]; + const newArtifacts = [ + { + name: 'job-3 my-new-artifact', + path: '/new/download/path', + }, + { + name: 'job-4 my-new-artifact-2', + path: '/new/download/path-two', + }, + { + name: 'job-5 my-new-artifact-3', + path: '/new/download/path-three', + }, + ]; const artifactItemTestId = 'artifact-item'; const artifactsEndpointPlaceholder = ':pipeline_artifacts_id'; const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`; @@ -59,8 +73,15 @@ describe('Pipeline Multi Actions Dropdown', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId); const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId); + const findAllArtifactItemsData = () => + wrapper.findAllByTestId(artifactItemTestId).wrappers.map((x) => ({ + path: x.attributes('href'), + name: x.text(), + })); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message'); + const findWarning = () => wrapper.findByTestId('artifacts-fetch-warning'); + const changePipelineId = (newId) => wrapper.setProps({ pipelineId: newId }); beforeEach(() => { mockAxios = new MockAdapter(axios); @@ -136,6 +157,80 @@ describe('Pipeline Multi Actions Dropdown', () => { expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path); expect(findFirstArtifactItem().text()).toBe(artifacts[0].name); }); + + describe('when opened again with new artifacts', () => { + describe('with a successful refetch', () => { + beforeEach(async () => { + mockAxios.resetHistory(); + mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: newArtifacts }); + + findDropdown().vm.$emit('show'); + await nextTick(); + }); + + it('should hide list and render a loading spinner on dropdown click', () => { + expect(findAllArtifactItems()).toHaveLength(0); + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('should not render warning or empty message while loading', () => { + expect(findEmptyMessage().exists()).toBe(false); + expect(findWarning().exists()).toBe(false); + }); + + it('should render the correct new list', async () => { + await waitForPromises(); + + expect(findAllArtifactItemsData()).toEqual(newArtifacts); + }); + }); + + describe('with a failing refetch', () => { + beforeEach(async () => { + mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + findDropdown().vm.$emit('show'); + await waitForPromises(); + }); + + it('should render warning', () => { + expect(findWarning().text()).toBe(i18n.artifactsFetchWarningMessage); + }); + + it('should render old list', () => { + expect(findAllArtifactItemsData()).toEqual(artifacts); + }); + }); + }); + + describe('pipeline id has changed', () => { + const newEndpoint = artifactsEndpoint.replace( + artifactsEndpointPlaceholder, + pipelineId + 1, + ); + + beforeEach(() => { + changePipelineId(pipelineId + 1); + }); + + describe('followed by a failing request', () => { + beforeEach(async () => { + mockAxios.onGet(newEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + findDropdown().vm.$emit('show'); + await waitForPromises(); + }); + + it('should render error message and no warning', () => { + expect(findWarning().exists()).toBe(false); + expect(findAlert().text()).toBe(i18n.artifactsFetchErrorMessage); + }); + + it('should clear list', () => { + expect(findAllArtifactItems()).toHaveLength(0); + }); + }); + }); }); describe('artifacts list is empty', () => {