Commit 3cb9c129 authored by Vitaly Slobodin's avatar Vitaly Slobodin

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

Lazy load artifacts dropdown on pipeline list page

See merge request gitlab-org/gitlab!60058
parents e6f76139 5ae213c7
<script> <script>
import { import {
GlAlert,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlLoadingIcon,
GlSprintf, GlSprintf,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __ } from '~/locale'; 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.'),
};
export default { export default {
i18n: { i18n,
artifacts: __('Artifacts'),
downloadArtifact: __('Download %{name} artifact'),
artifactSectionHeader: __('Download artifacts'),
},
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { components: {
GlAlert,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader, GlDropdownSectionHeader,
GlLoadingIcon,
GlSprintf, GlSprintf,
}, },
inject: {
artifactsEndpoint: {
default: '',
},
artifactsEndpointPlaceholder: {
default: '',
},
},
props: { props: {
artifacts: { pipelineId: {
type: Array, type: Number,
required: true, required: true,
}, },
}, },
data() {
return {
artifacts: [],
hasError: false,
isLoading: false,
};
},
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>
...@@ -43,11 +87,18 @@ export default { ...@@ -43,11 +87,18 @@ export default {
lazy lazy
text-sr-only text-sr-only
no-caret no-caret
@show.once="fetchArtifacts"
> >
<gl-dropdown-section-header>{{ <gl-dropdown-section-header>{{
$options.i18n.artifactSectionHeader $options.i18n.artifactSectionHeader
}}</gl-dropdown-section-header> }}</gl-dropdown-section-header>
<gl-alert v-if="hasError" variant="danger" :dismissible="false">
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
<gl-loading-icon v-if="isLoading" />
<gl-dropdown-item <gl-dropdown-item
v-for="(artifact, i) in artifacts" v-for="(artifact, i) in artifacts"
:key="i" :key="i"
......
...@@ -54,6 +54,11 @@ export default { ...@@ -54,6 +54,11 @@ export default {
isCancelling() { isCancelling() {
return this.cancelingPipeline === this.pipeline.id; return this.cancelingPipeline === this.pipeline.id;
}, },
showArtifacts() {
return (
this.pipeline.details.artifacts?.length || this.pipeline.details.has_downloadable_artifacts
);
},
}, },
watch: { watch: {
pipeline() { pipeline() {
...@@ -110,10 +115,7 @@ export default { ...@@ -110,10 +115,7 @@ export default {
@click="handleCancelClick" @click="handleCancelClick"
/> />
<pipeline-multi-actions <pipeline-multi-actions v-if="showArtifacts" :pipeline-id="pipeline.id" />
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -22,6 +22,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { ...@@ -22,6 +22,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
const { const {
endpoint, endpoint,
artifactsEndpoint,
artifactsEndpointPlaceholder,
pipelineScheduleUrl, pipelineScheduleUrl,
emptyStateSvgPath, emptyStateSvgPath,
errorStateSvgPath, errorStateSvgPath,
...@@ -41,6 +43,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { ...@@ -41,6 +43,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
el, el,
provide: { provide: {
addCiYmlPath, addCiYmlPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates), suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
}, },
data() { data() {
......
- page_title _('Pipelines') - page_title _('Pipelines')
- add_page_specific_style 'page_bundles/pipelines' - add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/ci_status' - add_page_specific_style 'page_bundles/ci_status'
- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
= render_if_exists "shared/shared_runners_minutes_limit_flash_message" = render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
project_id: @project.id, project_id: @project.id,
params: params.to_json, params: params.to_json,
"artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
"artifacts-endpoint-placeholder" => artifacts_endpoint_placeholder,
"pipeline-schedule-url" => pipeline_schedules_path(@project), "pipeline-schedule-url" => pipeline_schedules_path(@project),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
......
---
title: Lazy load artifacts on pipelines list page
merge_request: 60058
author:
type: added
...@@ -23641,6 +23641,9 @@ msgstr "" ...@@ -23641,6 +23641,9 @@ msgstr ""
msgid "Pipelines|Copy trigger token" msgid "Pipelines|Copy trigger token"
msgstr "" msgstr ""
msgid "Pipelines|Could not load artifacts."
msgstr ""
msgid "Pipelines|Could not load merged YAML content" msgid "Pipelines|Could not load merged YAML content"
msgstr "" msgstr ""
......
import { GlDropdown, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlDropdown, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMultiActions from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineMultiActions, {
i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
describe('Pipeline Multi Actions Dropdown', () => { describe('Pipeline Multi Actions Dropdown', () => {
let wrapper; let wrapper;
let mockAxios;
const artifacts = [
{
name: 'job my-artifact',
path: '/download/path',
},
{
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
];
const artifactItemTestId = 'artifact-item'; const artifactItemTestId = 'artifact-item';
const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
const defaultProps = { const createComponent = ({ mockData = {} } = {}) => {
artifacts: [
{
name: 'job my-artifact',
path: '/download/path',
},
{
name: 'job-2 my-artifact-2',
path: '/download/path-two',
},
],
};
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(PipelineMultiActions, { shallowMount(PipelineMultiActions, {
provide: {
artifactsEndpoint,
artifactsEndpointPlaceholder,
},
propsData: { propsData: {
...defaultProps, pipelineId,
...props, },
data() {
return {
...mockData,
};
}, },
stubs: { stubs: {
GlSprintf, GlSprintf,
...@@ -35,33 +49,64 @@ describe('Pipeline Multi Actions Dropdown', () => { ...@@ -35,33 +49,64 @@ describe('Pipeline Multi Actions Dropdown', () => {
); );
}; };
const findAlert = () => wrapper.findComponent(GlAlert);
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId); const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId); const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
beforeEach(() => { beforeEach(() => {
createComponent(); mockAxios = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
mockAxios.restore();
wrapper.destroy(); wrapper.destroy();
}); });
it('should render the dropdown', () => { it('should render the dropdown', () => {
createComponent();
expect(findDropdown().exists()).toBe(true); expect(findDropdown().exists()).toBe(true);
}); });
describe('Artifacts', () => { describe('Artifacts', () => {
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 all the provided artifacts', () => { it('should render all the provided artifacts', () => {
expect(findAllArtifactItems()).toHaveLength(defaultProps.artifacts.length); createComponent({ mockData: { artifacts } });
expect(findAllArtifactItems()).toHaveLength(artifacts.length);
}); });
it('should render the correct artifact name and path', () => { it('should render the correct artifact name and path', () => {
expect(findFirstArtifactItem().attributes('href')).toBe(defaultProps.artifacts[0].path); createComponent({ mockData: { artifacts } });
expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
expect(findFirstArtifactItem().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();
expect(findFirstArtifactItem().text()).toBe( const error = findAlert();
`Download ${defaultProps.artifacts[0].name} artifact`, expect(error.exists()).toBe(true);
); expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
});
}); });
}); });
}); });
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