diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-02-28 16:51:04 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-02-28 16:51:04 +0000 |
commit | 15c8fafaef2a7dba24f5de45346067ccd4694c15 (patch) | |
tree | c650ade5c009268408052dca2a7872ed56452977 | |
parent | 4371f845649deaf6bf31f0a675b33f1d58f64de4 (diff) | |
download | gitlab-ce-38587-pipeline-empty-state.tar.gz |
First iteration of cleaning up Pipelines code [ci skip]38587-pipeline-empty-state
7 files changed, 419 insertions, 1 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index e741789fbb6..5146d53cd50 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -269,7 +269,7 @@ export const normalizeCRLFHeaders = (headers) => { * @param {Object} paginationInformation * @returns {Object} */ -export const parseIntPagination = paginationInformation => ({ +export const parseIntPagination = (paginationInformation = {}) => ({ perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), page: parseInt(paginationInformation['X-PAGE'], 10), total: parseInt(paginationInformation['X-TOTAL'], 10), diff --git a/app/assets/javascripts/pipelines/stores/pipelines/actions.js b/app/assets/javascripts/pipelines/stores/pipelines/actions.js new file mode 100644 index 00000000000..93bd664c14b --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/actions.js @@ -0,0 +1,218 @@ +import _ from 'underscore'; +import axios from '../../../lib/utils/axios_utils'; +import createFlash from '../../../flash'; +import { __ } from '../../../locale'; +import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils'; +import * as types from './mutation_types'; + +/** + * SYNC ACTIONS - BASE DATA + */ + +export const setEndpoint = ({ commit }, endpoint) => { + commit(types.SET_ENDPOINT, endpoint); +}; + +export const setRailsData = ({ commit }, dataset) => { + commit(types.SET_PATHS, dataset); + commit(types.SET_PERMISSIONS, dataset); +}; + +/** + * Both the store and the components are shared between two apps. + * 1. Main app is rendered in /pipelines route + * 2. Child App is rendered in the following pages: + * 1. merge_requests/pipelines + * 2. commits/pipelines + * 3. new merge request/pipelines (create route) + * @param {Object} + * @param {String} type main || child + */ +export const setViewType = ({ commit }, type) => { + commit(types.SET_VIEW_TYPE, type); +}; + +export const setHasCI = ({ commit }, value) => { + commit(types.SET_HAS_CI, value); +}; + +/** + * SYNC ACTIONS - DYNAMIC DATA + */ + +/** + * Commits a mutation to store the pipelines. + * Detached to allow being shared between two applications: + * - Main Pipeline View + * - MR & Commits View + * @param {Object} + */ +export const setPipelines = ({ commit }, pipelines) => { + commit(types.SET_PIPELINES, pipelines); +}; + +/** + * Commits a mutation to store the counts used in the tabs in the Main Pipelines view. + * @param {Object} + */ +export const setCount = ({ commit }, count) => { + commit(types.SET_COUNT, count); +}; + +/** + * Commits a mutation to store the pagination used in the Main Pipelines view. + * @param {Object} + */ +export const setPagination = ({ commit }, pagination = {}) => { + commit(types.SET_PAGINATION, pagination); +}; + +export const showLoading = ({ commit }) => { + commit(types.SHOW_LOADING); +}; + +export const hideLoading = ({ commit }) => { + commit(types.HIDE_LOADING); +}; + +export const toggleError = ({ commit }, value) => { + commit(types.SET_HAS_ERROR, value); +}; + +export const toggleIsMakingRequest = ({ commit }, value) => { + commit(types.SET_IS_MAKING_REQUEST, value); +}; + +export const toggleHasMadeRequest = ({ commit }, value) => { + commit(types.SET_HAS_MADE_REQUEST, value); +}; + +export const toggleShouldUpdateGraphDropdown = ({ commit }, value) => { + commit(types.SET_SHOULD_UPDATE_GRAPH_DROPDOWN, value); +}; + +/** + * Used to handle state between pagination, tabs and URL + * + * @param {Object} + * @param {String} scope + */ +export const setScope = ({ commit }, scope) => { + commit(types.SET_SCOPE, scope); +}; + +/** + * Used to handle state between pagination, tabs and URL + * + * @param {Object} + * @param {String} page + */ +export const setPage = ({ commit }, page) => { + commit(types.SET_SCOPE, page); +}; + +/** + * Used to update the polling class with the correct state + * between pagination, tabs and URL + * + * @param {Object} + * @param {Object} requestData + */ +export const setRequestData = ({ commit }, requestData) => { + commit(types.SET_REQUEST_DATA, requestData); +}; + +/** + * ASYNC ACTIONS + */ + + /* +** +* axios callbacks are detached from the main action to allow to work with the polling function. +* Actions will be dispacted in the polling callback. +* +* Actions that set data are divided to allow the usage in two different apps. +* They should be the same in the future once the MR and Commits pipelines table allow pagination. +*/ + +/** +* Fetched the given endpoint and parameters. +* Used in polling method and in the refresh pipelines method. +* +* @returns {Promise} +*/ +export const getPipelines = ({ state }) => axios + .get(state.endpoint, { scope: state.scope, page: state.page }); + +/** +* Commits mutations for the common data shared between two applications: +* - Main Pipeline View +* - MR & Commits View +* @param {Object} +*/ +export const successCallback = ({ dispatch, commit, state }, response) => { + dispatch('hideLoading'); + dispatch('shouldUpdateGraphDropdown', true); + dispatch('toggleHasMadeRequest', true); + + if (state.type === 'main') { + if (_.isEqual(parseQueryStringIntoObject(response.url.split('?')[1]), state.requestData)) { + dispatch('setPagination', response.headers); + dispatch('setCount', response.data.count); + commit(types.SET_PIPELINES, response.data.pipelines); + } + } else if (state.type === 'child') { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.data.pipelines || response.data; + commit(types.SET_PIPELINES, pipelines); + dispatch('emitEventUpdateCount', response); + } +}; + +export const errorCallback = ({ dispatch }) => { + dispatch('hideLoading'); + dispatch('shouldUpdateGraphDropdown', true); + dispatch('toggleError', true); +}; + +// TODO HANDLE THIS! +export const emitEventUpdateCount = (store, response) => { + const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { + detail: { + pipelines: response, + }, + }); + + // notifiy to update the count in tabs + // if (this.$el.parentElement) { + // this.$el.parentElement.dispatchEvent(updatePipelinesEvent); + // } +}; + +export const stopPipeline = ({ dispatch, state, commit }, endpoint) => axios.post(`${endpoint}.json`) + .then(() => { + dispatch('refreshPipeline'); + }) + .catch(() => { + createFlash(__('An error occurred while trying to stop the pipeline. Please try again.')); + }); + +export const retryPipeline = ({ dispatch, state, commit }, endpoint) => axios.post(`${endpoint}.json`) + .then(() => { + dispatch('refreshPipeline'); + }) + .catch(() => { + createFlash(__('An error occurred while trying to retry the pipeline. Please try again.')); + }); + +export const refreshPipeline = ({ state, dispatch }) => { + if (!state.isMakingRequest) { + dispatch('showLoading'); + dispatch('getPipelines') + .then(response => dispatch('successCallback', response)) + .catch(() => { + dispatch('errorCallback'); + createFlash(__('An error occurred while fetching the pipelines. Please try again.')); + }); + } +}; diff --git a/app/assets/javascripts/pipelines/stores/pipelines/getters.js b/app/assets/javascripts/pipelines/stores/pipelines/getters.js new file mode 100644 index 00000000000..5db4baecfc8 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/getters.js @@ -0,0 +1,24 @@ +export const shouldRenderErrorState = state => state.hasError && !state.isLoading; + +export const shouldRenderEmptyState = (state) => { + if (state.viewType === 'main') { + return !state.isLoading && + !state.hasError && + !state.pipelines.length && + state.scope !== 'all' && + state.scope !== null; + } + + return !state.pipelines.length && + !state.isLoading && + state.hasMadeRequest && + !state.hasError; +}; + +export const shouldRenderPipelinesTable = state => !state.isLoading && + !state.hasError && + state.pipelines.length; + +export const shouldRenderRunPipelineButton = state => state.permissions.canCreatePipeline && state.hasCI; + +export const shouldRenderClearCacheButton = state => state.permissions.
\ No newline at end of file diff --git a/app/assets/javascripts/pipelines/stores/pipelines/index.js b/app/assets/javascripts/pipelines/stores/pipelines/index.js new file mode 100644 index 00000000000..aef8d91dfbc --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as mutations from './mutations'; +import * as getters from './getters'; +import state from './state'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + mutations, + state, +}); + diff --git a/app/assets/javascripts/pipelines/stores/pipelines/mutation_types.js b/app/assets/javascripts/pipelines/stores/pipelines/mutation_types.js new file mode 100644 index 00000000000..5da8602cadf --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/mutation_types.js @@ -0,0 +1,22 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; +export const SET_PERMISSIONS = 'SET_RAILS_DATA'; +export const SET_PATHS = 'SET_RAILS_DATA'; +export const SET_VIEW_TYPE = 'SET_VIEW_TYPE'; +export const SET_HAS_CI = 'SET_VIEW_TYPE'; + +export const SHOW_LOADING = 'SHOW_LOADING'; +export const HIDE_LOADING = 'HIDE_LOADING'; + +export const SET_HAS_ERROR = 'SET_HAS_ERROR'; +export const SET_IS_MAKING_REQUEST = 'SET_IS_MAKING_REQUEST'; +export const SET_HAS_MADE_REQUEST = 'SET_HAS_MADE_REQUEST'; +export const SET_SHOULD_UPDATE_GRAPH_DROPDOWN = 'SET_SHOULD_UPDATE_GRAPH_DROPDOWN'; + +export const SET_SCOPE = 'SET_SCOPE'; +export const SET_PAGE = 'SET_PAGE'; +export const SET_REQUEST_DATA = 'SET_REQUEST_DATA'; + +export const SET_PIPELINES = 'SET_PIPELINES'; +export const SET_PAGINATION = 'SET_PAGINATION'; +export const SET_COUNT = 'SET_COUNT'; + diff --git a/app/assets/javascripts/pipelines/stores/pipelines/mutations.js b/app/assets/javascripts/pipelines/stores/pipelines/mutations.js new file mode 100644 index 00000000000..51b3596ec58 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/mutations.js @@ -0,0 +1,102 @@ +import * as types from './mutation_types'; +import { + parseIntPagination, + normalizeHeaders, + convertPermissionToBoolean, +} from '../../../lib/utils/common_utils'; + +export default { + /** + * Because it's used in several different routes the endpoint may vary: + * 1. In the new merge request view the endpoint won't include `.json` + * 1. In the MR view, Commits view and Pipelines view it will include `.json` + */ + [types.SET_ENDPOINT](state, endpoint) { + let parsedEndpoint; + + if (endpoint.indexOf('.json') === -1) { + parsedEndpoint = `${endpoint}.json`; + } else { + parsedEndpoint = endpoint; + } + + Object.assign(state, { endpoint: parsedEndpoint }); + }, + + [types.SET_PERMISSIONS](state, dataset) { + Object.assign(state, { + canCreatePipeline: convertPermissionToBoolean(dataset.canCreatePipeline), + canResetCache: dataset.resetCachePath !== null || dataset.resetCachePath !== undefined, + }); + }, + + [types.SET_PATHS](state, dataset) { + Object.assign(state, { + helpPagePath: dataset.helpPagePath, + autoDevopsHelpPath: dataset.autoDevopsHelpPath, + newPipelinePath: dataset.newPipelinePath, + ciLintPath: dataset.ciLintPath, + resetCachePath: dataset.resetCachePath, + emptyStateSvgPath: dataset.emptyStateSvgPath, + errorStateSvgPath: dataset.errorStateSvgPath, + }); + }, + + [types.SET_VIEW_TYPE](state, viewType) { + Object.assign(state, { viewType }); + }, + + [types.SET_HAS_CI](state, value) { + Object.assign(state, { hasCI: value }); + }, + + [types.SHOW_LOADING](state) { + Object.assign(state, { isLoading: true }); + }, + + [types.HIDE_LOADING](state) { + Object.assign(state, { isLoading: false }); + }, + + [types.SET_HAS_ERROR](state, value) { + Object.assign(state, { hasError: value }); + }, + + [types.SET_IS_MAKING_REQUEST](state, value) { + Object.assign(state, { isMakingRequest: value }); + }, + + [types.SET_HAS_MADE_REQUEST](state, value) { + Object.assign(state, { hasMadeRequest: value }); + }, + + [types.SET_SHOULD_UPDATE_GRAPH_DROPDOWN](state, value) { + Object.assign(state, { updateGraphDropdown: value }); + }, + + [types.SET_SCOPE](state, value) { + Object.assign(state, { scope: value }); + }, + + [types.SET_PAGE](state, value) { + Object.assign(state, { page: value }); + }, + + [types.SET_REQUEST_DATA](state, value) { + Object.assign(state, { requestData: value }); + }, + + [types.SET_PIPELINES](state, pipelines) { + Object.assign(state, { pipelines }); + }, + + [types.SET_PAGINATION](state, pagination) { + const parsedPagination = parseIntPagination(normalizeHeaders(pagination)); + + Object.assign(state, { pagination: parsedPagination }); + }, + + [types.SET_COUNT](state, pipelines) { + Object.assign(state, { pipelines }); + }, +}; diff --git a/app/assets/javascripts/pipelines/stores/pipelines/state.js b/app/assets/javascripts/pipelines/stores/pipelines/state.js new file mode 100644 index 00000000000..88428222920 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/pipelines/state.js @@ -0,0 +1,36 @@ +export default { + endpoint: null, + + permissions: { + canCreatePipeline: false, + canResetCache: false, + }, + + paths: { + helpPagePath: null, + newPipelinePath: null, + ciLintPath: null, + emptyStateSvgPath: null, + errorStateSvgPath: null, + autoDevopsPath: null, + resetCachePath: null, + }, + + viewType: 'main', // 'main || child' + + hasCI: false, + + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + + scope: 'all', + page: '1', + requestData: {}, + + pipelines: [], + count: {}, + pagination: {}, +}; |