Commit 0f67f5f7 authored by Scott Hampton's avatar Scott Hampton Committed by Filipa Lacerda

Fix add/remove pipeline dashboard update delay

Force update when a project is added or removed.

Also fix skeleton loading regression.
parent fd6e94ab
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui'; import { GlDashboardSkeleton, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import DashboardProject from './project.vue'; import DashboardProject from './project.vue';
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
components: { components: {
DashboardProject, DashboardProject,
GlModal, GlModal,
GlLoadingIcon, GlDashboardSkeleton,
GlButton, GlButton,
ProjectSelector, ProjectSelector,
}, },
......
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="dashboard-card card border-0"> <div class="js-dashboard-project dashboard-card card border-0">
<project-header <project-header
:project="project" :project="project"
:has-pipeline-failed="hasPipelineFailed" :has-pipeline-failed="hasPipelineFailed"
...@@ -117,7 +117,7 @@ export default { ...@@ -117,7 +117,7 @@ export default {
/> />
</div> </div>
<div class="col-10 col-sm-6 pr-0 pl-5 align-self-center align-middle ci-table"> <div class="col-10 col-sm-7 pr-0 pl-5 align-self-center align-middle ci-table">
<commit <commit
:tag="commitRef.tag" :tag="commitRef.tag"
:commit-ref="commitRef" :commit-ref="commitRef"
...@@ -129,7 +129,7 @@ export default { ...@@ -129,7 +129,7 @@ export default {
/> />
</div> </div>
<div class="col-sm-5 pl-0 text-right align-self-center d-none d-sm-block"> <div class="col-sm-4 pl-0 text-right align-self-center d-none d-sm-block">
<time-ago <time-ago
v-if="shouldShowTimeAgo" v-if="shouldShowTimeAgo"
:time="finishedTime" :time="finishedTime"
......
...@@ -61,14 +61,14 @@ export default { ...@@ -61,14 +61,14 @@ export default {
</gl-link> </gl-link>
</div> </div>
<div class="dropdown js-more-actions"> <div class="dropdown js-more-actions">
<gl-button <button
v-gl-tooltip v-gl-tooltip
class="js-more-actions-toggle d-flex align-items-center bg-transparent border-0 p-0 ml-2" class="js-more-actions-toggle d-flex align-items-center bg-transparent border-0 p-0 ml-2"
data-toggle="dropdown" data-toggle="dropdown"
:title="__('More actions')" :title="__('More actions')"
> >
<icon name="ellipsis_v" class="text-secondary" /> <icon name="ellipsis_v" class="text-secondary" />
</gl-button> </button>
<ul class="dropdown-menu dropdown-menu-right"> <ul class="dropdown-menu dropdown-menu-right">
<li> <li>
<gl-button class="btn btn-transparent js-remove-button" @click="onRemove"> <gl-button class="btn btn-transparent js-remove-button" @click="onRemove">
......
...@@ -76,7 +76,7 @@ export const receiveAddProjectsToDashboardSuccess = ({ dispatch, state }, data) ...@@ -76,7 +76,7 @@ export const receiveAddProjectsToDashboardSuccess = ({ dispatch, state }, data)
} }
if (added.length) { if (added.length) {
dispatch('fetchProjects'); dispatch('forceProjectsRequest');
} }
}; };
...@@ -135,7 +135,7 @@ export const removeProject = ({ dispatch }, removePath) => { ...@@ -135,7 +135,7 @@ export const removeProject = ({ dispatch }, removePath) => {
.catch(() => dispatch('receiveRemoveProjectError')); .catch(() => dispatch('receiveRemoveProjectError'));
}; };
export const receiveRemoveProjectSuccess = ({ dispatch }) => dispatch('fetchProjects'); export const receiveRemoveProjectSuccess = ({ dispatch }) => dispatch('forceProjectsRequest');
export const receiveRemoveProjectError = () => { export const receiveRemoveProjectError = () => {
createFlash(__('Something went wrong, unable to remove project')); createFlash(__('Something went wrong, unable to remove project'));
......
---
title: Fix add/remove pipeline dashboard issue
merge_request: 11029
author:
type: fixed
import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter';
import store from 'ee/operations/store/index'; import axios from '~/lib/utils/axios_utils';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Project from 'ee/operations/components/dashboard/project.vue';
import Dashboard from 'ee/operations/components/dashboard/dashboard.vue'; import Dashboard from 'ee/operations/components/dashboard/dashboard.vue';
import DashboardProject from 'ee/operations/components/dashboard/project.vue'; import store from 'ee/operations/store';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { trimText } from 'spec/helpers/vue_component_helper'; import { trimText } from 'spec/helpers/vue_component_helper';
import { getChildInstances, clearState } from '../../helpers';
import { mockProjectData, mockText } from '../../mock_data'; import { mockProjectData, mockText } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('dashboard component', () => { describe('dashboard component', () => {
const DashboardComponent = Vue.extend(Dashboard); const mockAddEndpoint = 'mock-addPath';
const DashboardProjectComponent = Vue.extend(DashboardProject); const mockListEndpoint = 'mock-listPath';
const mount = () => const DashboardComponent = localVue.extend(Dashboard);
new DashboardComponent({ let wrapper;
let mockAxios;
const mountComponent = () =>
mount(DashboardComponent, {
sync: false,
store, store,
localVue,
propsData: { propsData: {
addPath: 'mock-addPath', addPath: mockAddEndpoint,
listPath: 'mock-listPath', listPath: mockListEndpoint,
emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg', emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg',
emptyDashboardHelpPath: '/help/user/operations_dashboard/index.html', emptyDashboardHelpPath: '/help/user/operations_dashboard/index.html',
}, },
methods: { });
fetchProjects: () => {},
},
}).$mount();
let vm;
beforeEach(() => { beforeEach(() => {
vm = mount(); mockAxios = new MockAdapter(axios);
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: mockProjectData(1) });
wrapper = mountComponent();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
clearState(store); mockAxios.restore();
}); });
it('renders dashboard title', () => { it('renders dashboard title', () => {
expect(vm.$el.querySelector('.js-dashboard-title').innerText.trim()).toBe( const dashboardTitle = wrapper.element.querySelector('.js-dashboard-title');
mockText.DASHBOARD_TITLE,
); expect(dashboardTitle.innerText.trim()).toEqual(mockText.DASHBOARD_TITLE);
}); });
describe('add projects button', () => { describe('add projects button', () => {
let button; let button;
beforeEach(() => { beforeEach(() => {
button = vm.$el.querySelector('.js-add-projects-button'); button = wrapper.element.querySelector('.js-add-projects-button');
}); });
it('renders add projects text', () => { it('renders add projects text', () => {
...@@ -53,7 +63,31 @@ describe('dashboard component', () => { ...@@ -53,7 +63,31 @@ describe('dashboard component', () => {
it('renders the projects modal', () => { it('renders the projects modal', () => {
button.click(); button.click();
expect(vm.$el.querySelector('.add-projects-modal')).toBeDefined(); expect(wrapper.element.querySelector('.add-projects-modal')).toBeDefined();
});
describe('when a project is added', () => {
it('immediately requests the project list again', done => {
mockAxios.reset();
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: mockProjectData(2) });
mockAxios.onPost(mockAddEndpoint).replyOnce(200, { added: [1], invalid: [] });
wrapper.vm
.$nextTick()
.then(() => {
wrapper.vm.projectClicked({ id: 1 });
})
.then(timeoutPromise)
.then(() => {
wrapper.vm.onOk();
})
.then(timeoutPromise)
.then(() => {
expect(store.state.projects.length).toEqual(2);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(2);
done();
})
.catch(done.fail);
});
}); });
}); });
...@@ -64,53 +98,77 @@ describe('dashboard component', () => { ...@@ -64,53 +98,77 @@ describe('dashboard component', () => {
beforeEach(() => { beforeEach(() => {
store.state.projects = projects; store.state.projects = projects;
vm = mount(); wrapper = mountComponent();
}); });
it('includes a dashboard project component for each project', () => { it('includes a dashboard project component for each project', () => {
expect(getChildInstances(vm, DashboardProjectComponent).length).toBe(projectCount); const projectComponents = wrapper.element.querySelectorAll('.js-dashboard-project');
expect(projectComponents.length).toBe(projectCount);
}); });
it('passes each project to the dashboard project component', () => { it('passes each project to the dashboard project component', () => {
const [oneProject] = projects; const [oneProject] = projects;
const [projectComponent] = getChildInstances(vm, DashboardProjectComponent); const projectComponent = wrapper.find(Project);
expect(projectComponent.project).toEqual(oneProject); expect(projectComponent.props().project).toEqual(oneProject);
});
describe('when a project is removed', () => {
it('immediately requests the project list again', done => {
mockAxios.reset();
mockAxios.onDelete(projects[0].remove_path).reply(200);
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: [] });
wrapper.find('.js-remove-button').vm.$emit('click');
timeoutPromise()
.then(() => {
expect(store.state.projects.length).toEqual(0);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(0);
done();
})
.catch(done.fail);
});
}); });
}); });
describe('empty state', () => { describe('empty state', () => {
beforeEach(() => { beforeEach(() => {
store.state.projects = []; store.state.projects = [];
vm = mount(); mockAxios.reset();
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: [] });
wrapper = mountComponent();
}); });
it('renders empty state svg after requesting projects with no results', () => { it('renders empty state svg after requesting projects with no results', () => {
const svgSrc = vm.$el.querySelector('.js-empty-state-svg').src; const svgSrc = wrapper.element.querySelector('.js-empty-state-svg').src;
expect(svgSrc).toMatch(mockText.EMPTY_SVG_SOURCE); expect(svgSrc).toMatch(mockText.EMPTY_SVG_SOURCE);
}); });
it('renders title', () => { it('renders title', () => {
expect(vm.$el.querySelector('.js-title').innerText.trim()).toBe(mockText.EMPTY_TITLE); expect(wrapper.element.querySelector('.js-title').innerText.trim()).toBe(
mockText.EMPTY_TITLE,
);
}); });
it('renders sub-title', () => { it('renders sub-title', () => {
expect(trimText(vm.$el.querySelector('.js-sub-title').innerText)).toBe( expect(trimText(wrapper.element.querySelector('.js-sub-title').innerText)).toBe(
mockText.EMPTY_SUBTITLE, mockText.EMPTY_SUBTITLE,
); );
}); });
it('renders link to documentation', () => { it('renders link to documentation', () => {
const link = vm.$el.querySelector('.js-documentation-link'); const link = wrapper.element.querySelector('.js-documentation-link');
expect(link.innerText.trim()).toBe('More information'); expect(link.innerText.trim()).toBe('More information');
}); });
it('links to documentation', () => { it('links to documentation', () => {
const link = vm.$el.querySelector('.js-documentation-link'); const link = wrapper.element.querySelector('.js-documentation-link');
expect(link.href).toMatch(vm.emptyDashboardHelpPath); expect(link.href).toMatch(wrapper.props().emptyDashboardHelpPath);
}); });
}); });
}); });
......
...@@ -22,6 +22,7 @@ describe('actions', () => { ...@@ -22,6 +22,7 @@ describe('actions', () => {
afterEach(() => { afterEach(() => {
clearState(store); clearState(store);
mockAxios.restore(); mockAxios.restore();
actions.clearProjectsEtagPoll();
}); });
describe('addProjectsToDashboard', () => { describe('addProjectsToDashboard', () => {
...@@ -109,7 +110,7 @@ describe('actions', () => { ...@@ -109,7 +110,7 @@ describe('actions', () => {
[], [],
[ [
{ {
type: 'fetchProjects', type: 'forceProjectsRequest',
}, },
], ],
done, done,
...@@ -189,10 +190,6 @@ describe('actions', () => { ...@@ -189,10 +190,6 @@ describe('actions', () => {
}); });
describe('fetchProjects', () => { describe('fetchProjects', () => {
afterEach(() => {
actions.clearProjectsEtagPoll();
});
it('calls project list endpoint', done => { it('calls project list endpoint', done => {
store.state.projectEndpoints.list = mockListEndpoint; store.state.projectEndpoints.list = mockListEndpoint;
mockAxios.onGet(mockListEndpoint).replyOnce(200); mockAxios.onGet(mockListEndpoint).replyOnce(200);
...@@ -312,7 +309,7 @@ describe('actions', () => { ...@@ -312,7 +309,7 @@ describe('actions', () => {
null, null,
null, null,
[], [],
[{ type: 'fetchProjects' }], [{ type: 'forceProjectsRequest' }],
done, done,
); );
}); });
......
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