Commit 4b909f29 authored by Mark Florian's avatar Mark Florian

Merge branch '55252-fix-fe-job-app-timeout-in-state' into 'master'

Step 1 - Fix `job_app` initialization and cleaning up of side-effects

See merge request gitlab-org/gitlab!23838
parents ab45ba0c 0e4ac7a6
...@@ -8,7 +8,6 @@ import { polyfillSticky } from '~/lib/utils/sticky'; ...@@ -8,7 +8,6 @@ import { polyfillSticky } from '~/lib/utils/sticky';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
...@@ -22,7 +21,6 @@ import { isNewJobLogActive } from '../store/utils'; ...@@ -22,7 +21,6 @@ import { isNewJobLogActive } from '../store/utils';
export default { export default {
name: 'JobPageApp', name: 'JobPageApp',
store: createStore(),
components: { components: {
CiHeader, CiHeader,
Callout, Callout,
...@@ -60,27 +58,15 @@ export default { ...@@ -60,27 +58,15 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
endpoint: {
type: String,
required: true,
},
terminalPath: { terminalPath: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
pagePath: {
type: String,
required: true,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
}, },
logState: {
type: String,
required: true,
},
subscriptionsMoreMinutesUrl: { subscriptionsMoreMinutesUrl: {
type: String, type: String,
required: false, required: false,
...@@ -161,37 +147,28 @@ export default { ...@@ -161,37 +147,28 @@ export default {
created() { created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100); this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint);
this.setTraceOptions({
logState: this.logState,
pagePath: this.pagePath,
});
this.fetchJob();
this.fetchTrace();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll); window.addEventListener('scroll', this.updateScroll);
}, },
mounted() { mounted() {
this.updateSidebar(); this.updateSidebar();
}, },
destroyed() { beforeDestroy() {
this.stopPollingTrace();
this.stopPolling();
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll); window.removeEventListener('scroll', this.updateScroll);
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchJobsForStage', 'fetchJobsForStage',
'hideSidebar', 'hideSidebar',
'showSidebar', 'showSidebar',
'toggleSidebar', 'toggleSidebar',
'fetchTrace',
'scrollBottom', 'scrollBottom',
'scrollTop', 'scrollTop',
'stopPollingTrace',
'stopPolling',
'toggleScrollButtons', 'toggleScrollButtons',
'toggleScrollAnimation', 'toggleScrollAnimation',
]), ]),
...@@ -223,7 +200,7 @@ export default { ...@@ -223,7 +200,7 @@ export default {
<div> <div>
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:size="2" size="lg"
class="js-job-loading qa-loading-animation prepend-top-20" class="js-job-loading qa-loading-animation prepend-top-20"
/> />
......
import Vue from 'vue'; import Vue from 'vue';
import JobApp from './components/job_app.vue'; import JobApp from './components/job_app.vue';
import createStore from './store';
export default () => { export default () => {
const element = document.getElementById('js-job-vue-app'); const element = document.getElementById('js-job-vue-app');
const store = createStore();
// Let's start initializing the store (i.e. fetching data) right away
store.dispatch('init', element.dataset);
return new Vue({ return new Vue({
el: element, el: element,
store,
components: { components: {
JobApp, JobApp,
}, },
......
...@@ -14,6 +14,16 @@ import { ...@@ -14,6 +14,16 @@ import {
scrollUp, scrollUp,
} from '~/lib/utils/scroll_utils'; } from '~/lib/utils/scroll_utils';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
dispatch('setJobEndpoint', endpoint);
dispatch('setTraceOptions', {
logState,
pagePath,
});
return Promise.all([dispatch('fetchJob'), dispatch('fetchTrace')]);
};
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options); export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options);
...@@ -147,7 +157,6 @@ export const toggleScrollisInBottom = ({ commit }, toggle) => { ...@@ -147,7 +157,6 @@ export const toggleScrollisInBottom = ({ commit }, toggle) => {
export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
let traceTimeout;
export const fetchTrace = ({ dispatch, state }) => export const fetchTrace = ({ dispatch, state }) =>
axios axios
.get(`${state.traceEndpoint}/trace.json`, { .get(`${state.traceEndpoint}/trace.json`, {
...@@ -157,24 +166,32 @@ export const fetchTrace = ({ dispatch, state }) => ...@@ -157,24 +166,32 @@ export const fetchTrace = ({ dispatch, state }) =>
dispatch('toggleScrollisInBottom', isScrolledToBottom()); dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data); dispatch('receiveTraceSuccess', data);
if (!data.complete) { if (data.complete) {
traceTimeout = setTimeout(() => {
dispatch('fetchTrace');
}, 4000);
} else {
dispatch('stopPollingTrace'); dispatch('stopPollingTrace');
} else if (!state.traceTimeout) {
dispatch('startPollingTrace');
} }
}) })
.catch(() => dispatch('receiveTraceError')); .catch(() => dispatch('receiveTraceError'));
export const stopPollingTrace = ({ commit }) => { export const startPollingTrace = ({ dispatch, commit }) => {
const traceTimeout = setTimeout(() => {
commit(types.SET_TRACE_TIMEOUT, 0);
dispatch('fetchTrace');
}, 4000);
commit(types.SET_TRACE_TIMEOUT, traceTimeout);
};
export const stopPollingTrace = ({ state, commit }) => {
clearTimeout(state.traceTimeout);
commit(types.SET_TRACE_TIMEOUT, 0);
commit(types.STOP_POLLING_TRACE); commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout);
}; };
export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log); export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
export const receiveTraceError = ({ commit }) => { export const receiveTraceError = ({ dispatch }) => {
commit(types.RECEIVE_TRACE_ERROR); dispatch('stopPollingTrace');
clearTimeout(traceTimeout);
flash(__('An error occurred while fetching the job log.')); flash(__('An error occurred while fetching the job log.'));
}; };
/** /**
......
...@@ -10,7 +10,6 @@ export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM'; ...@@ -10,7 +10,6 @@ export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM';
export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP'; export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP';
export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM'; export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP'; export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
// TODO
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION'; export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM'; export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
...@@ -20,6 +19,7 @@ export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; ...@@ -20,6 +19,7 @@ export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR'; export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
export const REQUEST_TRACE = 'REQUEST_TRACE'; export const REQUEST_TRACE = 'REQUEST_TRACE';
export const SET_TRACE_TIMEOUT = 'SET_TRACE_TIMEOUT';
export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
......
...@@ -53,17 +53,14 @@ export default { ...@@ -53,17 +53,14 @@ export default {
state.isTraceComplete = log.complete || state.isTraceComplete; state.isTraceComplete = log.complete || state.isTraceComplete;
}, },
/** [types.SET_TRACE_TIMEOUT](state, id) {
* Will remove loading animation state.traceTimeout = id;
*/
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
}, },
/** /**
* Will remove loading animation * Will remove loading animation
*/ */
[types.RECEIVE_TRACE_ERROR](state) { [types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true; state.isTraceComplete = true;
}, },
......
...@@ -22,6 +22,7 @@ export default () => ({ ...@@ -22,6 +22,7 @@ export default () => ({
isTraceComplete: false, isTraceComplete: false,
traceSize: 0, traceSize: 0,
isTraceSizeVisible: false, isTraceSizeVisible: false,
traceTimeout: 0,
// used as a query parameter to fetch the trace // used as a query parameter to fetch the trace
traceState: null, traceState: null,
......
...@@ -157,17 +157,21 @@ describe('Jobs Store Mutations', () => { ...@@ -157,17 +157,21 @@ describe('Jobs Store Mutations', () => {
}); });
}); });
describe('STOP_POLLING_TRACE', () => { describe('SET_TRACE_TIMEOUT', () => {
it('sets isTraceComplete to true', () => { it('sets the traceTimeout id', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy); const id = 7;
expect(stateCopy.isTraceComplete).toEqual(true); expect(stateCopy.traceTimeout).not.toEqual(id);
mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
expect(stateCopy.traceTimeout).toEqual(id);
}); });
}); });
describe('RECEIVE_TRACE_ERROR', () => { describe('STOP_POLLING_TRACE', () => {
it('resets trace state and sets error to true', () => { it('sets isTraceComplete to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy); mutations[types.STOP_POLLING_TRACE](stateCopy);
expect(stateCopy.isTraceComplete).toEqual(true); expect(stateCopy.isTraceComplete).toEqual(true);
}); });
......
...@@ -6,7 +6,6 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,7 +6,6 @@ import axios from '~/lib/utils/axios_utils';
import jobApp from '~/jobs/components/job_app.vue'; import jobApp from '~/jobs/components/job_app.vue';
import createStore from '~/jobs/store'; import createStore from '~/jobs/store';
import * as types from '~/jobs/store/mutation_types'; import * as types from '~/jobs/store/mutation_types';
import { resetStore } from '../store/helpers';
import job from '../mock_data'; import job from '../mock_data';
describe('Job App ', () => { describe('Job App ', () => {
...@@ -16,24 +15,29 @@ describe('Job App ', () => { ...@@ -16,24 +15,29 @@ describe('Job App ', () => {
let vm; let vm;
let mock; let mock;
const props = { const initSettings = {
endpoint: `${gl.TEST_HOST}jobs/123.json`, endpoint: `${gl.TEST_HOST}jobs/123.json`,
pagePath: `${gl.TEST_HOST}jobs/123`,
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
const props = {
runnerHelpUrl: 'help/runner', runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment', deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners', runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables', variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal', terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
projectPath: 'user-name/project-name', projectPath: 'user-name/project-name',
subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes', subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes',
logState:
'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
}; };
const waitForJobReceived = () => waitForMutation(store, types.RECEIVE_JOB_SUCCESS); const waitForJobReceived = () => waitForMutation(store, types.RECEIVE_JOB_SUCCESS);
const setupAndMount = ({ jobData = {}, traceData = {} } = {}) => { const setupAndMount = ({ jobData = {}, traceData = {} } = {}) => {
mock.onGet(props.endpoint).replyOnce(200, { ...job, ...jobData }); mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
mock.onGet(`${props.pagePath}/trace.json`).reply(200, traceData); mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, traceData);
store.dispatch('init', initSettings);
vm = mountComponentWithStore(Component, { props, store }); vm = mountComponentWithStore(Component, { props, store });
...@@ -46,7 +50,6 @@ describe('Job App ', () => { ...@@ -46,7 +50,6 @@ describe('Job App ', () => {
}); });
afterEach(() => { afterEach(() => {
resetStore(store);
vm.$destroy(); vm.$destroy();
mock.restore(); mock.restore();
}); });
...@@ -384,7 +387,6 @@ describe('Job App ', () => { ...@@ -384,7 +387,6 @@ describe('Job App ', () => {
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
done();
}); });
it('displays remaining time for a delayed job', done => { it('displays remaining time for a delayed job', done => {
......
...@@ -15,6 +15,7 @@ import { ...@@ -15,6 +15,7 @@ import {
scrollBottom, scrollBottom,
requestTrace, requestTrace,
fetchTrace, fetchTrace,
startPollingTrace,
stopPollingTrace, stopPollingTrace,
receiveTraceSuccess, receiveTraceSuccess,
receiveTraceError, receiveTraceError,
...@@ -241,6 +242,50 @@ describe('Job State actions', () => { ...@@ -241,6 +242,50 @@ describe('Job State actions', () => {
done, done,
); );
}); });
describe('when job is incomplete', () => {
let tracePayload;
beforeEach(() => {
tracePayload = {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: false,
};
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
});
it('dispatches startPollingTrace', done => {
testAction(
fetchTrace,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
{ type: 'receiveTraceSuccess', payload: tracePayload },
{ type: 'startPollingTrace' },
],
done,
);
});
it('does not dispatch startPollingTrace when timeout is non-empty', done => {
mockedState.traceTimeout = 1;
testAction(
fetchTrace,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
{ type: 'receiveTraceSuccess', payload: tracePayload },
],
done,
);
});
});
}); });
describe('error', () => { describe('error', () => {
...@@ -265,16 +310,69 @@ describe('Job State actions', () => { ...@@ -265,16 +310,69 @@ describe('Job State actions', () => {
}); });
}); });
describe('startPollingTrace', () => {
let dispatch;
let commit;
beforeEach(() => {
jasmine.clock().install();
dispatch = jasmine.createSpy();
commit = jasmine.createSpy();
startPollingTrace({ dispatch, commit });
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should save the timeout id but not call fetchTrace', () => {
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 1);
expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
});
describe('after timeout has passed', () => {
beforeEach(() => {
jasmine.clock().tick(4000);
});
it('should clear the timeout id and fetchTrace', () => {
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
expect(dispatch).toHaveBeenCalledWith('fetchTrace');
});
});
});
describe('stopPollingTrace', () => { describe('stopPollingTrace', () => {
let origTimeout;
beforeEach(() => {
// Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727
origTimeout = window.clearTimeout;
window.clearTimeout = jasmine.createSpy();
});
afterEach(() => {
window.clearTimeout = origTimeout;
});
it('should commit STOP_POLLING_TRACE mutation ', done => { it('should commit STOP_POLLING_TRACE mutation ', done => {
const traceTimeout = 7;
testAction( testAction(
stopPollingTrace, stopPollingTrace,
null, null,
mockedState, { ...mockedState, traceTimeout },
[{ type: types.STOP_POLLING_TRACE }], [{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
[], [],
done, )
); .then(() => {
expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -292,15 +390,8 @@ describe('Job State actions', () => { ...@@ -292,15 +390,8 @@ describe('Job State actions', () => {
}); });
describe('receiveTraceError', () => { describe('receiveTraceError', () => {
it('should commit RECEIVE_TRACE_ERROR mutation ', done => { it('should commit stop polling trace', done => {
testAction( testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
receiveTraceError,
null,
mockedState,
[{ type: types.RECEIVE_TRACE_ERROR }],
[],
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