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>
import { GlDropdown, GlDropdownItem, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import {
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 {
i18n,
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlAlert,
GlDropdown,
GlDropdownItem,
GlLoadingIcon,
GlSprintf,
},
translations: {
artifacts: __('Artifacts'),
downloadArtifact: __('Download %{name} artifact'),
inject: {
artifactsEndpoint: {
default: '',
},
artifactsEndpointPlaceholder: {
default: '',
},
},
props: {
artifacts: {
type: Array,
pipelineId: {
type: Number,
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>
<template>
<gl-dropdown
v-gl-tooltip
class="build-artifacts js-pipeline-dropdown-download"
:title="$options.translations.artifacts"
:text="$options.translations.artifacts"
:aria-label="$options.translations.artifacts"
:title="$options.i18n.artifacts"
:text="$options.i18n.artifacts"
:aria-label="$options.i18n.artifacts"
icon="download"
right
lazy
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
v-for="(artifact, i) in artifacts"
:key="i"
......@@ -42,7 +109,7 @@ export default {
rel="nofollow"
download
>
<gl-sprintf :message="$options.translations.downloadArtifact">
<gl-sprintf :message="$options.i18n.downloadArtifact">
<template #name>{{ artifact.name }}</template>
</gl-sprintf>
</gl-dropdown-item>
......
......@@ -107,9 +107,6 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
hasArtifacts() {
return this.pipeline?.details?.artifacts?.length > 0;
},
isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
},
......@@ -288,11 +285,7 @@ export default {
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
<pipeline-artifacts
v-if="hasArtifacts"
:artifacts="pipeline.details.artifacts"
class="gl-ml-3"
/>
<pipeline-artifacts :pipeline-id="pipeline.id" class="gl-ml-3" />
</span>
</div>
</div>
......
......@@ -32,6 +32,10 @@ export default () => {
const vm = new Vue({
el: '#js-vue-mr-widget',
provide: {
artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
},
...MrWidgetOptions,
apolloProvider,
});
......
- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
= javascript_tag do
:plain
window.gl = window.gl || {};
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.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')}';
......
---
title: Lazy load artifacts dropdown in pipelines merge request widget
merge_request: 61055
author:
type: added
......@@ -23860,6 +23860,9 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
msgid "Pipelines|No artifacts available"
msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
......
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { GlAlert, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
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', () => {
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, {
provide: {
artifactsEndpoint,
artifactsEndpointPlaceholder,
},
propsData: {
artifacts: [
{
name: 'job my-artifact',
path: '/download/path',
},
{
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
],
pipelineId,
},
data() {
return {
...mockData,
};
},
stubs: {
GlSprintf,
......@@ -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 findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
beforeEach(() => {
createComponent();
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
......@@ -37,13 +60,66 @@ describe('Pipelines Artifacts dropdown', () => {
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', () => {
expect(findAllGlDropdownItems()).toHaveLength(2);
createComponent({ mockData: { artifacts } });
expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
});
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