diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-14 09:07:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-14 09:07:54 +0000 |
commit | 5ff1b520badaa2da217416964709f49f3ede350a (patch) | |
tree | 821ddbc4062a56fd2a7f26a0776457da3e074859 /app/assets | |
parent | eccfaf7c242ab8afec22cdaf68865763e780fdeb (diff) | |
download | gitlab-ce-5ff1b520badaa2da217416964709f49f3ede350a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
11 files changed, 304 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue new file mode 100644 index 00000000000..dc766176617 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue @@ -0,0 +1,40 @@ +<script> +import { GlLink } from '@gitlab/ui'; + +export default { + components: { + GlLink, + }, + props: { + artifacts: { + type: Array, + required: true, + }, + }, +}; +</script> +<template> + <table class="table m-0"> + <thead class="thead-white text-nowrap"> + <tr class="d-none d-sm-table-row"> + <th class="w-0"></th> + <th>{{ __('Artifact') }}</th> + <th class="w-50"></th> + <th>{{ __('Job') }}</th> + </tr> + </thead> + + <tbody> + <tr v-for="item in artifacts" :key="item.text"> + <td class="w-0"></td> + <td> + <gl-link :href="item.url" target="_blank">{{ item.text }}</gl-link> + </td> + <td class="w-0"></td> + <td> + <gl-link :href="item.job_path">{{ item.job_name }}</gl-link> + </td> + </tr> + </tbody> + </table> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue new file mode 100644 index 00000000000..730e67761be --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue @@ -0,0 +1,36 @@ +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import ArtifactsList from './artifacts_list.vue'; +import MrCollapsibleExtension from './mr_collapsible_extension.vue'; +import createStore from '../stores/artifacts_list'; + +export default { + store: createStore(), + components: { + ArtifactsList, + MrCollapsibleExtension, + }, + props: { + endpoint: { + type: String, + required: true, + }, + }, + computed: { + ...mapState(['artifacts', 'isLoading', 'hasError']), + ...mapGetters(['title']), + }, + created() { + this.setEndpoint(this.endpoint); + this.fetchArtifacts(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchArtifacts']), + }, +}; +</script> +<template> + <mr-collapsible-extension :title="title" :is-loading="isLoading" :has-error="hasError"> + <artifacts-list :artifacts="artifacts" /> + </mr-collapsible-extension> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue new file mode 100644 index 00000000000..ae1d9368008 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue @@ -0,0 +1,81 @@ +<script> +import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlButton, + GlLink, + GlLoadingIcon, + Icon, + }, + props: { + title: { + type: String, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + hasError: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isCollapsed: true, + }; + }, + + computed: { + arrowIconName() { + return this.isCollapsed ? 'angle-right' : 'angle-down'; + }, + ariaLabel() { + return this.isCollapsed ? __('Expand') : __('Collapse'); + }, + isButtonDisabled() { + return this.isLoading || this.hasError; + }, + }, + methods: { + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + }, +}; +</script> +<template> + <div> + <div class="mr-widget-extension d-flex align-items-center pl-3"> + <gl-button + class="btn-blank btn s32 square append-right-default" + :aria-label="ariaLabel" + :disabled="isButtonDisabled" + @click="toggleCollapsed" + > + <gl-loading-icon v-if="isLoading" /> + <icon v-else :name="arrowIconName" class="js-icon" /> + </gl-button> + <gl-button + variant="link" + class="js-title" + :disabled="isButtonDisabled" + :class="{ 'border-0': isButtonDisabled }" + @click="toggleCollapsed" + > + <template v-if="isCollapsed">{{ title }}</template> + <template v-else>{{ __('Collapse') }}</template> + </gl-button> + </div> + + <div v-if="!isCollapsed" class="border-top js-slot-container"> + <slot></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 8fdf61a6b8d..ffc3e0967d4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,5 +1,6 @@ <script> import _ from 'underscore'; +import ArtifactsApp from './artifacts_list_app.vue'; import Deployment from './deployment.vue'; import MrWidgetContainer from './mr_widget_container.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue'; @@ -15,6 +16,7 @@ import MrWidgetPipeline from './mr_widget_pipeline.vue'; export default { name: 'MrWidgetPipelineContainer', components: { + ArtifactsApp, Deployment, MrWidgetContainer, MrWidgetPipeline, @@ -79,6 +81,9 @@ export default { :troubleshooting-docs-path="mr.troubleshootingDocsPath" /> <template v-slot:footer> + <div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts"> + <artifacts-app :endpoint="mr.exposedArtifactsPath" /> + </div> <div v-if="deployments.length" class="mr-widget-extension"> <deployment v-for="deployment in deployments" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js new file mode 100644 index 00000000000..3648db795f5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js @@ -0,0 +1,74 @@ +import Visibility from 'visibilityjs'; +import axios from '~/lib/utils/axios_utils'; +import Poll from '~/lib/utils/poll'; +import httpStatusCodes from '~/lib/utils/http_status'; + +import * as types from './mutation_types'; + +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +export const requestArtifacts = ({ commit }) => commit(types.REQUEST_ARTIFACTS); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const fetchArtifacts = ({ state, dispatch }) => { + dispatch('requestArtifacts'); + + eTagPoll = new Poll({ + resource: { + getArtifacts(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.endpoint, + method: 'getArtifacts', + successCallback: ({ data, status }) => { + dispatch('receiveArtifactsSuccess', { + data, + status, + }); + }, + errorCallback: () => dispatch('receiveArtifactsError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.endpoint) + .then(({ data, status }) => dispatch('receiveArtifactsSuccess', { data, status })) + .catch(() => dispatch('receiveArtifactsError')); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveArtifactsSuccess = ({ commit }, response) => { + // With 204 we keep polling and don't update the state + if (response.status === httpStatusCodes.OK) { + commit(types.RECEIVE_ARTIFACTS_SUCCESS, response.data); + } +}; + +export const receiveArtifactsError = ({ commit }) => commit(types.RECEIVE_ARTIFACTS_ERROR); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js new file mode 100644 index 00000000000..8921637b93b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js @@ -0,0 +1,16 @@ +import { s__, n__ } from '~/locale'; + +export const title = state => { + if (state.isLoading) { + return s__('BuildArtifacts|Loading artifacts'); + } + + if (state.hasError) { + return s__('BuildArtifacts|An error occurred while fetching the artifacts'); + } + + return n__('View exposed artifact', 'View %d exposed artifacts', state.artifacts.length); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js new file mode 100644 index 00000000000..f8abbc99f0f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import * as getters from './getters'; +import state from './state'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js new file mode 100644 index 00000000000..282faf6f8a4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; + +export const REQUEST_ARTIFACTS = 'REQUEST_ARTIFACTS'; +export const RECEIVE_ARTIFACTS_SUCCESS = 'RECEIVE_ARTIFACTS_SUCCESS'; +export const RECEIVE_ARTIFACTS_ERROR = 'RECEIVE_ARTIFACTS_ERROR'; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js new file mode 100644 index 00000000000..95a091f1bd6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js @@ -0,0 +1,22 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.REQUEST_ARTIFACTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_ARTIFACTS_SUCCESS](state, response) { + state.hasError = false; + state.isLoading = false; + + state.artifacts = response; + }, + [types.RECEIVE_ARTIFACTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + + state.artifacts = []; + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js new file mode 100644 index 00000000000..92dad171b1b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js @@ -0,0 +1,8 @@ +export default () => ({ + endpoint: null, + + isLoading: false, + hasError: false, + + artifacts: [], +}); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 699d41494bf..99d32ffe424 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -100,6 +100,7 @@ export default class MergeRequestStore { this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; this.testResultsPath = data.test_reports_path; + this.exposedArtifactsPath = data.exposed_artifacts_path; this.cancelAutoMergePath = data.cancel_auto_merge_path; this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path); |