Commit d79218fc authored by Phil Hughes's avatar Phil Hughes

Merge branch '50101-vuex-store' into 'master'

Adds Vuex store for the job log page

See merge request gitlab-org/gitlab-ce!21312
parents e550b1ab 55582b43
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import { setCiStatusFavicon } from '../../lib/utils/common_utils';
import flash from '../../flash';
import { __ } from '../../locale';
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setTraceEndpoint = ({ commit }, endpoint) =>
commit(types.SET_TRACE_ENDPOINT, endpoint);
export const setStagesEndpoint = ({ commit }, endpoint) =>
commit(types.SET_STAGES_ENDPOINT, endpoint);
export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint);
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
export const requestJob = ({ commit }) => commit(types.REQUEST_JOB);
export const fetchJob = ({ state, dispatch }) => {
dispatch('requestJob');
eTagPoll = new Poll({
resource: {
getJob(endpoint) {
return axios.get(endpoint);
},
},
data: state.jobEndpoint,
method: 'getJob',
successCallback: ({ data }) => dispatch('receiveJobSuccess', data),
errorCallback: () => dispatch('receiveJobError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
axios
.get(state.jobEndpoint)
.then(({ data }) => dispatch('receiveJobSuccess', data))
.catch(() => dispatch('receiveJobError'));
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartPolling');
} else {
dispatch('stopPolling');
}
});
};
export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data);
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
};
/**
* Job's Trace
*/
export const scrollTop = ({ commit }) => {
commit(types.SCROLL_TO_TOP);
window.scrollTo({ top: 0 });
};
export const scrollBottom = ({ commit }) => {
commit(types.SCROLL_TO_BOTTOM);
window.scrollTo({ top: document.height });
};
export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE);
let traceTimeout;
export const fetchTrace = ({ dispatch, state }) => {
dispatch('requestTrace');
axios
.get(`${state.traceEndpoint}/trace.json`, {
params: { state: state.traceState },
})
.then(({ data }) => {
if (!state.fetchingStatusFavicon) {
dispatch('fetchFavicon');
}
dispatch('receiveTraceSuccess', data);
if (!data.complete) {
traceTimeout = setTimeout(() => {
dispatch('fetchTrace');
}, 4000);
} else {
dispatch('stopPollingTrace');
}
})
.catch(() => dispatch('receiveTraceError'));
};
export const stopPollingTrace = ({ commit }) => {
commit(types.STOP_POLLING_TRACE);
clearTimeout(traceTimeout);
};
export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
export const receiveTraceError = ({ commit }) => {
commit(types.RECEIVE_TRACE_ERROR);
clearTimeout(traceTimeout);
flash(__('An error occurred while fetching the job log.'));
};
export const fetchFavicon = ({ state, dispatch }) => {
dispatch('requestStatusFavicon');
setCiStatusFavicon(`${state.pagePath}/status.json`)
.then(() => dispatch('receiveStatusFaviconSuccess'))
.catch(() => dispatch('requestStatusFaviconError'));
};
export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON);
export const receiveStatusFaviconSuccess = ({ commit }) =>
commit(types.RECEIVE_STATUS_FAVICON_SUCCESS);
export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR);
/**
* Stages dropdown on sidebar
*/
export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES);
export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
axios
.get(state.stagesEndpoint)
.then(({ data }) => dispatch('receiveStagesSuccess', data))
.catch(() => dispatch('receiveStagesError'));
};
export const receiveStagesSuccess = ({ commit }, data) =>
commit(types.RECEIVE_STAGES_SUCCESS, data);
export const receiveStagesError = ({ commit }) => {
commit(types.RECEIVE_STAGES_ERROR);
flash(__('An error occurred while fetching stages.'));
};
/**
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
// On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => {
dispatch('setSelectedStage', stage);
dispatch('requestJobsForStage');
axios
.get(state.stageJobsEndpoint)
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data))
.catch(() => dispatch('receiveJobsForStageError'));
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data);
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
flash(__('An error occurred while fetching the jobs.'));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
state: state(),
});
export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT';
export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT';
export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT';
export const SCROLL_TO_TOP = 'SCROLL_TO_TOP';
export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM';
export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
export const REQUEST_TRACE = 'REQUEST_TRACE';
export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON';
export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS';
export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR';
export const REQUEST_STAGES = 'REQUEST_STAGES';
export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE';
export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS';
export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
[types.REQUEST_STATUS_FAVICON](state) {
state.fetchingStatusFavicon = true;
},
[types.RECEIVE_STATUS_FAVICON_SUCCESS](state) {
state.fetchingStatusFavicon = false;
},
[types.RECEIVE_STATUS_FAVICON_ERROR](state) {
state.fetchingStatusFavicon = false;
},
[types.RECEIVE_TRACE_SUCCESS](state, log) {
if (log.state) {
state.traceState = log.state;
}
if (log.append) {
state.trace += log.html;
state.traceSize += log.size;
} else {
state.trace = log.html;
state.traceSize = log.size;
}
if (state.traceSize < log.total) {
state.isTraceSizeVisible = true;
} else {
state.isTraceSizeVisible = false;
}
state.isTraceComplete = log.complete;
state.hasTraceError = false;
},
[types.STOP_POLLING_TRACE](state) {
state.isTraceComplete = true;
},
// todo_fl: check this.
[types.RECEIVE_TRACE_ERROR](state) {
state.isLoadingTrace = false;
state.isTraceComplete = true;
state.hasTraceError = true;
},
[types.REQUEST_JOB](state) {
state.isLoading = true;
},
[types.RECEIVE_JOB_SUCCESS](state, job) {
state.isLoading = false;
state.hasError = false;
state.job = job;
},
[types.RECEIVE_JOB_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.job = {};
},
[types.SCROLL_TO_TOP](state) {
state.isTraceScrolledToBottom = false;
state.hasBeenScrolled = true;
},
[types.SCROLL_TO_BOTTOM](state) {
state.isTraceScrolledToBottom = true;
state.hasBeenScrolled = true;
},
[types.REQUEST_STAGES](state) {
state.isLoadingStages = true;
},
[types.RECEIVE_STAGES_SUCCESS](state, stages) {
state.isLoadingStages = false;
state.stages = stages;
},
[types.RECEIVE_STAGES_ERROR](state) {
state.isLoadingStages = false;
state.stages = [];
},
[types.REQUEST_JOBS_FOR_STAGE](state) {
state.isLoadingJobs = true;
},
[types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) {
state.isLoadingJobs = false;
state.jobs = jobs;
},
[types.RECEIVE_JOBS_FOR_STAGE_ERROR](state) {
state.isLoadingJobs = false;
state.jobs = [];
},
};
export default () => ({
jobEndpoint: null,
traceEndpoint: null,
// dropdown options
stagesEndpoint: null,
// list of jobs on sidebard
stageJobsEndpoint: null,
// job log
isLoading: false,
hasError: false,
job: {},
// trace
isLoadingTrace: false,
hasTraceError: false,
trace: '',
isTraceScrolledToBottom: false,
hasBeenScrolled: false,
isTraceComplete: false,
traceSize: 0, // todo_fl: needs to be converted into human readable format in components
isTraceSizeVisible: false,
fetchingStatusFavicon: false,
// used as a query parameter
traceState: null,
// used to check if we need to redirect the user - todo_fl: check if actually needed
traceStatus: null,
// sidebar dropdown
isLoadingStages: false,
isLoadingJobs: false,
selectedStage: null,
stages: [],
jobs: [],
});
...@@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl => ...@@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl =>
} }
return resetFavicon(); return resetFavicon();
}) })
.catch(resetFavicon); .catch((error) => {
resetFavicon();
throw error;
});
export const spriteIcon = (icon, className = '') => { export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : ''; const classAttribute = className.length > 0 ? `class="${className}"` : '';
......
...@@ -505,6 +505,18 @@ msgstr "" ...@@ -505,6 +505,18 @@ msgstr ""
msgid "An error occurred while fetching sidebar data" msgid "An error occurred while fetching sidebar data"
msgstr "" msgstr ""
msgid "An error occurred while fetching stages."
msgstr ""
msgid "An error occurred while fetching the job log."
msgstr ""
msgid "An error occurred while fetching the job."
msgstr ""
msgid "An error occurred while fetching the jobs."
msgstr ""
msgid "An error occurred while fetching the pipeline." msgid "An error occurred while fetching the pipeline."
msgstr "" msgstr ""
......
This diff is collapsed.
import state from '~/jobs/store/state';
import mutations from '~/jobs/store/mutations';
import * as types from '~/jobs/store/mutation_types';
describe('Jobs Store Mutations', () => {
let stateCopy;
const html =
'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I';
beforeEach(() => {
stateCopy = state();
});
describe('REQUEST_STATUS_FAVICON', () => {
it('should set fetchingStatusFavicon to true', () => {
mutations[types.REQUEST_STATUS_FAVICON](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(true);
});
});
describe('RECEIVE_STATUS_FAVICON_SUCCESS', () => {
it('should set fetchingStatusFavicon to false', () => {
mutations[types.RECEIVE_STATUS_FAVICON_SUCCESS](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false);
});
});
describe('RECEIVE_STATUS_FAVICON_ERROR', () => {
it('should set fetchingStatusFavicon to false', () => {
mutations[types.RECEIVE_STATUS_FAVICON_ERROR](stateCopy);
expect(stateCopy.fetchingStatusFavicon).toEqual(false);
});
});
describe('RECEIVE_TRACE_SUCCESS', () => {
describe('when trace has state', () => {
it('sets traceState', () => {
const stateLog =
'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0=';
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
state: stateLog,
});
expect(stateCopy.traceState).toEqual(stateLog);
});
});
describe('when traceSize is smaller than the total size', () => {
it('sets isTraceSizeVisible to true', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { total: 51184600, size: 1231 });
expect(stateCopy.isTraceSizeVisible).toEqual(true);
});
});
describe('when traceSize is bigger than the total size', () => {
it('sets isTraceSizeVisible to false', () => {
const copy = Object.assign({}, stateCopy, { traceSize: 5118460, size: 2321312 });
mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 });
expect(copy.isTraceSizeVisible).toEqual(false);
});
});
it('sets trace, trace size and isTraceComplete', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: true,
html,
size: 511846,
complete: true,
});
expect(stateCopy.trace).toEqual(html);
expect(stateCopy.traceSize).toEqual(511846);
expect(stateCopy.isTraceComplete).toEqual(true);
});
});
describe('STOP_POLLING_TRACE', () => {
it('sets isTraceComplete to true', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy);
expect(stateCopy.isTraceComplete).toEqual(true);
});
});
describe('RECEIVE_TRACE_ERROR', () => {
it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
expect(stateCopy.isLoadingTrace).toEqual(false);
expect(stateCopy.isTraceComplete).toEqual(true);
expect(stateCopy.hasTraceError).toEqual(true);
});
});
describe('REQUEST_JOB', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_JOB](stateCopy);
expect(stateCopy.isLoading).toEqual(true);
});
});
describe('RECEIVE_JOB_SUCCESS', () => {
beforeEach(() => {
mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 });
});
it('sets is loading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('sets hasError to false', () => {
expect(stateCopy.hasError).toEqual(false);
});
it('sets job data', () => {
expect(stateCopy.job).toEqual({ id: 1312321 });
});
});
describe('RECEIVE_JOB_ERROR', () => {
it('resets job data', () => {
mutations[types.RECEIVE_JOB_ERROR](stateCopy);
expect(stateCopy.isLoading).toEqual(false);
expect(stateCopy.hasError).toEqual(true);
expect(stateCopy.job).toEqual({});
});
});
describe('SCROLL_TO_TOP', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_TOP](stateCopy);
});
it('sets isTraceScrolledToBottom to false', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(false);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('SCROLL_TO_BOTTOM', () => {
beforeEach(() => {
mutations[types.SCROLL_TO_BOTTOM](stateCopy);
});
it('sets isTraceScrolledToBottom to true', () => {
expect(stateCopy.isTraceScrolledToBottom).toEqual(true);
});
it('sets hasBeenScrolled to true', () => {
expect(stateCopy.hasBeenScrolled).toEqual(true);
});
});
describe('REQUEST_STAGES', () => {
it('sets isLoadingStages to true', () => {
mutations[types.REQUEST_STAGES](stateCopy);
expect(stateCopy.isLoadingStages).toEqual(true);
});
});
describe('RECEIVE_STAGES_SUCCESS', () => {
beforeEach(() => {
mutations[types.RECEIVE_STAGES_SUCCESS](stateCopy, [{ name: 'build' }]);
});
it('sets isLoadingStages to false', () => {
expect(stateCopy.isLoadingStages).toEqual(false);
});
it('sets stages', () => {
expect(stateCopy.stages).toEqual([{ name: 'build' }]);
});
});
describe('RECEIVE_STAGES_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_STAGES_ERROR](stateCopy);
});
it('sets isLoadingStages to false', () => {
expect(stateCopy.isLoadingStages).toEqual(false);
});
it('resets stages', () => {
expect(stateCopy.stages).toEqual([]);
});
});
describe('REQUEST_JOBS_FOR_STAGE', () => {
it('sets isLoadingStages to true', () => {
mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy);
expect(stateCopy.isLoadingJobs).toEqual(true);
});
});
describe('RECEIVE_JOBS_FOR_STAGE_SUCCESS', () => {
beforeEach(() => {
mutations[types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](stateCopy, [{ name: 'karma' }]);
});
it('sets isLoadingJobs to false', () => {
expect(stateCopy.isLoadingJobs).toEqual(false);
});
it('sets jobs', () => {
expect(stateCopy.jobs).toEqual([{ name: 'karma' }]);
});
});
describe('RECEIVE_JOBS_FOR_STAGE_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_JOBS_FOR_STAGE_ERROR](stateCopy);
});
it('sets isLoadingJobs to false', () => {
expect(stateCopy.isLoadingJobs).toEqual(false);
});
it('resets jobs', () => {
expect(stateCopy.jobs).toEqual([]);
});
});
});
...@@ -403,6 +403,7 @@ describe('common_utils', () => { ...@@ -403,6 +403,7 @@ describe('common_utils', () => {
afterEach(() => { afterEach(() => {
document.body.removeChild(document.getElementById('favicon')); document.body.removeChild(document.getElementById('favicon'));
}); });
it('should set page favicon to provided favicon', () => { it('should set page favicon to provided favicon', () => {
const faviconPath = '//custom_favicon'; const faviconPath = '//custom_favicon';
commonUtils.setFavicon(faviconPath); commonUtils.setFavicon(faviconPath);
...@@ -479,17 +480,14 @@ describe('common_utils', () => { ...@@ -479,17 +480,14 @@ describe('common_utils', () => {
}); });
it('should reset favicon in case of error', (done) => { it('should reset favicon in case of error', (done) => {
mock.onGet(BUILD_URL).networkError(); mock.onGet(BUILD_URL).replyOnce(500);
commonUtils.setCiStatusFavicon(BUILD_URL) commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => { .catch(() => {
const favicon = document.getElementById('favicon'); const favicon = document.getElementById('favicon');
expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
done(); done();
}) });
// Error is already caught in catch() block of setCiStatusFavicon,
// It won't throw another error for us to catch
.catch(done.fail);
}); });
it('should set page favicon to CI status favicon based on provided status', (done) => { it('should set page favicon to CI status favicon based on provided status', (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