Commit 35b0630a authored by Phil Hughes's avatar Phil Hughes

Merge branch 'sh-lazy-load-pipeline-mr-widget-artifacts' into 'master'

Lazy load artifacts on pipeline MR widget

See merge request gitlab-org/gitlab!61055
parents 85b73092 b5f4630a
<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
...@@ -23884,6 +23884,9 @@ msgstr "" ...@@ -23884,6 +23884,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