Commit b5f4630a authored by Scott Hampton's avatar Scott Hampton

Lazy load artifacts on pipeline MR widget

Lazy load the artifacts on the pipeline MR widget when clicking on the
dropdown button.

Changelog: added
parent cf5e9f0f
<script> <script>
import { GlDropdown, GlDropdownItem, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import {
import { __ } from '~/locale'; GlAlert,
GlDropdown,
GlDropdownItem,
GlLoadingIcon,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
export const i18n = {
artifacts: __('Artifacts'),
downloadArtifact: __('Download %{name} artifact'),
artifactSectionHeader: __('Download artifacts'),
artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
noArtifacts: s__('Pipelines|No artifacts available'),
};
export default { export default {
i18n,
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { components: {
GlAlert,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlLoadingIcon,
GlSprintf, GlSprintf,
}, },
translations: { inject: {
artifacts: __('Artifacts'), artifactsEndpoint: {
downloadArtifact: __('Download %{name} artifact'), default: '',
},
artifactsEndpointPlaceholder: {
default: '',
},
}, },
props: { props: {
artifacts: { pipelineId: {
type: Array, type: Number,
required: true, required: true,
}, },
}, },
data() {
return {
artifacts: [],
hasError: false,
isLoading: false,
};
},
computed: {
hasArtifacts() {
return Boolean(this.artifacts.length);
},
},
methods: {
fetchArtifacts() {
this.isLoading = true;
// Replace the placeholder with the ID of the pipeline we are viewing
const endpoint = this.artifactsEndpoint.replace(
this.artifactsEndpointPlaceholder,
this.pipelineId,
);
return axios
.get(endpoint)
.then(({ data }) => {
this.artifacts = data.artifacts;
})
.catch(() => {
this.hasError = true;
})
.finally(() => {
this.isLoading = false;
});
},
},
}; };
</script> </script>
<template> <template>
<gl-dropdown <gl-dropdown
v-gl-tooltip v-gl-tooltip
class="build-artifacts js-pipeline-dropdown-download" class="build-artifacts js-pipeline-dropdown-download"
:title="$options.translations.artifacts" :title="$options.i18n.artifacts"
:text="$options.translations.artifacts" :text="$options.i18n.artifacts"
:aria-label="$options.translations.artifacts" :aria-label="$options.i18n.artifacts"
icon="download" icon="download"
right right
lazy lazy
text-sr-only text-sr-only
@show.once="fetchArtifacts"
> >
<gl-alert v-if="hasError" variant="danger" :dismissible="false">
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
<gl-loading-icon v-if="isLoading" />
<gl-alert v-else-if="!hasArtifacts" variant="info" :dismissible="false">
{{ $options.i18n.noArtifacts }}
</gl-alert>
<gl-dropdown-item <gl-dropdown-item
v-for="(artifact, i) in artifacts" v-for="(artifact, i) in artifacts"
:key="i" :key="i"
...@@ -42,7 +109,7 @@ export default { ...@@ -42,7 +109,7 @@ export default {
rel="nofollow" rel="nofollow"
download download
> >
<gl-sprintf :message="$options.translations.downloadArtifact"> <gl-sprintf :message="$options.i18n.downloadArtifact">
<template #name>{{ artifact.name }}</template> <template #name>{{ artifact.name }}</template>
</gl-sprintf> </gl-sprintf>
</gl-dropdown-item> </gl-dropdown-item>
......
...@@ -107,9 +107,6 @@ export default { ...@@ -107,9 +107,6 @@ export default {
hasCommitInfo() { hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
}, },
hasArtifacts() {
return this.pipeline?.details?.artifacts?.length > 0;
},
isMergeRequestPipeline() { isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
}, },
...@@ -288,11 +285,7 @@ export default { ...@@ -288,11 +285,7 @@ export default {
/> />
</span> </span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" /> <linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
<pipeline-artifacts <pipeline-artifacts :pipeline-id="pipeline.id" class="gl-ml-3" />
v-if="hasArtifacts"
:artifacts="pipeline.details.artifacts"
class="gl-ml-3"
/>
</span> </span>
</div> </div>
</div> </div>
......
...@@ -32,6 +32,10 @@ export default () => { ...@@ -32,6 +32,10 @@ export default () => {
const vm = new Vue({ const vm = new Vue({
el: '#js-vue-mr-widget', el: '#js-vue-mr-widget',
provide: {
artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
},
...MrWidgetOptions, ...MrWidgetOptions,
apolloProvider, apolloProvider,
}); });
......
- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
= javascript_tag do = javascript_tag do
:plain :plain
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)} window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
window.gl.mrWidgetData.artifacts_endpoint = '#{downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json)}';
window.gl.mrWidgetData.artifacts_endpoint_placeholder = '#{artifacts_endpoint_placeholder}';
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}'; window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviews/index.md', anchor: 'troubleshooting')}'; window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviews/index.md', anchor: 'troubleshooting')}';
......
---
title: Lazy load artifacts dropdown in pipelines merge request widget
merge_request: 61055
author:
type: added
...@@ -23860,6 +23860,9 @@ msgstr "" ...@@ -23860,6 +23860,9 @@ msgstr ""
msgid "Pipelines|More Information" msgid "Pipelines|More Information"
msgstr "" msgstr ""
msgid "Pipelines|No artifacts available"
msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above." msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr "" msgstr ""
......
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineArtifacts, {
i18n,
} from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => { describe('Pipelines Artifacts dropdown', () => {
let wrapper; let wrapper;
let mockAxios;
const createComponent = () => { const artifacts = [
{
name: 'job my-artifact',
path: '/download/path',
},
{
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
];
const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
const createComponent = ({ mockData = {} } = {}) => {
wrapper = shallowMount(PipelineArtifacts, { wrapper = shallowMount(PipelineArtifacts, {
provide: {
artifactsEndpoint,
artifactsEndpointPlaceholder,
},
propsData: { propsData: {
artifacts: [ pipelineId,
{ },
name: 'job my-artifact', data() {
path: '/download/path', return {
}, ...mockData,
{ };
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
],
}, },
stubs: { stubs: {
GlSprintf, GlSprintf,
...@@ -25,11 +45,14 @@ describe('Pipelines Artifacts dropdown', () => { ...@@ -25,11 +45,14 @@ describe('Pipelines Artifacts dropdown', () => {
}); });
}; };
const findAlert = () => wrapper.findComponent(GlAlert);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem); const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem);
const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem); const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
beforeEach(() => { beforeEach(() => {
createComponent(); mockAxios = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
...@@ -37,13 +60,66 @@ describe('Pipelines Artifacts dropdown', () => { ...@@ -37,13 +60,66 @@ describe('Pipelines Artifacts dropdown', () => {
wrapper = null; wrapper = null;
}); });
it('should render the dropdown', () => {
createComponent();
expect(findDropdown().exists()).toBe(true);
});
it('should fetch artifacts on dropdown click', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
expect(wrapper.vm.artifacts).toEqual(artifacts);
});
it('should render a dropdown with all the provided artifacts', () => { it('should render a dropdown with all the provided artifacts', () => {
expect(findAllGlDropdownItems()).toHaveLength(2); createComponent({ mockData: { artifacts } });
expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
}); });
it('should render a link with the provided path', () => { it('should render a link with the provided path', () => {
expect(findFirstGlDropdownItem().attributes('href')).toBe('/download/path'); createComponent({ mockData: { artifacts } });
expect(findFirstGlDropdownItem().text()).toBe('Download job my-artifact artifact'); expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
expect(findFirstGlDropdownItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
});
describe('with a failing request', () => {
it('should render an error message', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
mockAxios.onGet(endpoint).replyOnce(500);
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
const error = findAlert();
expect(error.exists()).toBe(true);
expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
});
});
describe('with no artifacts received', () => {
it('should render empty alert message', () => {
createComponent({ mockData: { artifacts: [] } });
const emptyAlert = findAlert();
expect(emptyAlert.exists()).toBe(true);
expect(emptyAlert.text()).toBe(i18n.noArtifacts);
});
});
describe('when artifacts are loading', () => {
it('should show loading icon', () => {
createComponent({ mockData: { isLoading: true } });
expect(findLoadingIcon().exists()).toBe(true);
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment