summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-14 09:07:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-14 09:07:54 +0000
commit5ff1b520badaa2da217416964709f49f3ede350a (patch)
tree821ddbc4062a56fd2a7f26a0776457da3e074859 /app/assets
parenteccfaf7c242ab8afec22cdaf68865763e780fdeb (diff)
downloadgitlab-ce-5ff1b520badaa2da217416964709f49f3ede350a.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue81
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
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);