summaryrefslogtreecommitdiff
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
parenteccfaf7c242ab8afec22cdaf68865763e780fdeb (diff)
downloadgitlab-ce-5ff1b520badaa2da217416964709f49f3ede350a.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml4
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-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
-rw-r--r--app/views/help/ui.html.haml3
-rw-r--r--changelogs/unreleased/15018-build-results-fe.yml5
-rw-r--r--doc/administration/gitaly/index.md8
-rw-r--r--doc/development/documentation/index.md34
-rw-r--r--doc/development/pipelines.md2
-rw-r--r--locale/gitlab.pot14
-rwxr-xr-xscripts/trigger-build-docs10
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js121
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_spec.js61
-rw-r--r--spec/frontend/vue_mr_widget/components/mock_data.js15
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js105
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js78
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js165
33 files changed, 949 insertions, 36 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fdd4c2a64f6..ba840d1ced0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
+image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
stages:
- prepare
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 87023da3c24..008151f889f 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -14,10 +14,10 @@
variables:
GIT_STRATEGY: none
environment:
- name: review-docs/$CI_COMMIT_REF_SLUG
+ name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693
- url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
before_script:
# We don't clone the repo by using GIT_STRATEGY: none and only download the
@@ -39,7 +39,7 @@ review-docs-deploy:
review-docs-cleanup:
extends: .review-docs
environment:
- name: review-docs/$CI_COMMIT_REF_SLUG
+ name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID
action: stop
script:
- ./trigger-build-docs cleanup
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index adbfe2b4675..a3a2ab0691f 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -13,7 +13,7 @@
- .default-before_script
- .assets-compile-cache
- .only-code-qa-changes
- image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 0c0591d3fdc..af7c7a0d152 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -123,7 +123,7 @@
- name: redis:alpine
.use-pg10:
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@@ -137,7 +137,7 @@
- name: docker.elastic.co/elasticsearch/elasticsearch:5.6.12
.use-pg10-ee:
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
diff --git a/Gemfile b/Gemfile
index 460d161587d..920f778c053 100644
--- a/Gemfile
+++ b/Gemfile
@@ -465,7 +465,7 @@ gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
# DNS Lookup
-gem 'net-dns', '~> 0.9.0'
+gem 'gitlab-net-dns', '~> 0.9.1'
# Countries list
gem 'countries', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index c6828ecb2cd..4285cfac45e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -370,6 +370,7 @@ GEM
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.0.0)
gitlab-markup (1.7.0)
+ gitlab-net-dns (0.9.1)
gitlab-peek (0.0.1)
railties (>= 4.0.0)
gitlab-sidekiq-fetcher (0.5.2)
@@ -596,7 +597,6 @@ GEM
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
nap (1.1.0)
- net-dns (0.9.0)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (5.2.0)
@@ -1173,6 +1173,7 @@ DEPENDENCIES
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
gitlab-markup (~> 1.7.0)
+ gitlab-net-dns (~> 0.9.1)
gitlab-peek (~> 0.0.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 2.7)
@@ -1222,7 +1223,6 @@ DEPENDENCIES
mini_magick
minitest (~> 5.11.0)
nakayoshi_fork (~> 0.0.4)
- net-dns (~> 0.9.0)
net-ldap
net-ntp
net-ssh (~> 5.2)
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);
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index cdc894ee5a0..b8a421ac9d3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -508,8 +508,7 @@
.btn-group
%a.btn Edit
%a.btn.btn-danger Remove
- .file-contenta.code
- = render 'shared/file_highlight', blob: blob
+ = render 'shared/file_highlight', blob: blob
%h2#markdown Markdown
%h4
diff --git a/changelogs/unreleased/15018-build-results-fe.yml b/changelogs/unreleased/15018-build-results-fe.yml
new file mode 100644
index 00000000000..6d619910937
--- /dev/null
+++ b/changelogs/unreleased/15018-build-results-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Creates Vue and Vuex app to render exposed artifacts
+merge_request: 17934
+author:
+type: added
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index b534e84191a..5dcdf0e85e9 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -377,6 +377,14 @@ The certificate to be used needs to be installed on all Gitaly nodes and on all
client nodes that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
+NOTE: **Note**
+The self-signed certificate must specify the address you use to access the
+Gitaly server. If you are addressing the Gitaly server by a hostname, you can
+either use the Common Name field for this, or add it as a Subject Alternative
+Name. If you are addressing the Gitaly server by its IP address, you must add it
+as a Subject Alternative Name to the certificate.
+[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
+
NOTE: **Note:**
It is possible to configure Gitaly servers with both an
unencrypted listening address `listen_addr` and an encrypted listening
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 9dead5cc0e2..fb0aa5130f8 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -300,17 +300,17 @@ You will need to push a branch to those repositories, it doesn't work for forks.
The `review-docs-deploy*` job will:
1. Create a new branch in the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs)
- project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`,
- where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for
- CE, etc.
-1. Trigger a cross project pipeline and build the docs site with your changes
-
-After a few minutes, the Review App will be deployed and you will be able to
-preview the changes. The docs URL can be found in two places:
-
-- In the merge request widget
-- In the output of the `review-docs-deploy*` job, which also includes the
- triggered pipeline so that you can investigate whether something went wrong
+ project named after the scheme: `docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID`,
+ where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ee` for
+ EE, `omnibus` for Omnibus GitLab, etc, and `CI_MERGE_REQUEST_IID` is the ID
+ of the respective merge request.
+1. Trigger a cross project pipeline and build the docs site with your changes.
+
+In case the review app URL returns 404, this means that either the site is not
+yet deployed, or something went wrong with the remote pipeline. Give it a few
+minutes and it should appear online, otherwise you can check the status of the
+remote pipeline from the link in the merge request's job output.
+If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
TIP: **Tip:**
Someone with no merge rights to the GitLab projects (think of forks from
@@ -343,12 +343,11 @@ If you want to know the in-depth details, here's what's really happening:
1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/trigger-build-docs)
script with the `deploy` flag, which in turn:
1. Takes your branch name and applies the following:
- - The slug of the branch name is used to avoid special characters since
- ultimately this will be used by NGINX.
- - The `preview-` prefix is added to avoid conflicts if there's a remote branch
- with the same name that you created in the merge request.
- - The final branch name is truncated to 42 characters to avoid filesystem
- limitations with long branch names (> 63 chars).
+ - The `docs-preview-` prefix is added.
+ - The product slug is used to know the project the review app originated
+ from.
+ - The number of the merge request is added so that you can know by the
+ `gitlab-docs` branch name the merge request it originated from.
1. The remote branch is then created if it doesn't exist (meaning you can
re-run the manual job as many times as you want and this step will be skipped).
1. A new cross-project pipeline is triggered in the docs project.
@@ -369,6 +368,7 @@ The following GitLab features are used among others:
- [Review Apps](../../ci/review_apps/index.md)
- [Artifacts](../../ci/yaml/README.md#artifacts)
- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
+- [Pipelines for merge requests](../../ci/merge_request_pipelines/index.md)
## Testing
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 5ec68ef3251..f2df6209492 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -38,7 +38,7 @@ The current stages are:
## Default image
The default image is currently
-`dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
+`gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
It includes Ruby 2.6.3, Go 1.11, Git 2.22, Chrome 73, Node 12, Yarn 1.16,
PostgreSQL 9.6, and Graphics Magick 1.3.33.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cd9208a434a..ae3c240419b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1964,6 +1964,9 @@ msgstr ""
msgid "Arrange charts"
msgstr ""
+msgid "Artifact"
+msgstr ""
+
msgid "Artifact ID"
msgstr ""
@@ -2661,6 +2664,12 @@ msgstr ""
msgid "Browse files"
msgstr ""
+msgid "BuildArtifacts|An error occurred while fetching the artifacts"
+msgstr ""
+
+msgid "BuildArtifacts|Loading artifacts"
+msgstr ""
+
msgid "Built-in"
msgstr ""
@@ -18067,6 +18076,11 @@ msgstr ""
msgid "View epics list"
msgstr ""
+msgid "View exposed artifact"
+msgid_plural "View %d exposed artifacts"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "View file @ "
msgstr ""
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index 046ee5bceb8..ea717d9ccbf 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -16,14 +16,12 @@ end
GITLAB_DOCS_REPO = 'gitlab-org/gitlab-docs'.freeze
#
-# Truncate the remote docs branch name otherwise we hit the filesystem
-# limit and the directory name where NGINX serves the site won't match
-# the branch name.
+# This is the branch that will be created in the gitlab-docs project.
+# Name it after the product we're previewing and the ID of the MR that
+# kicked the review app.
#
def docs_branch
- # The maximum string length a file can have on a filesystem (ext4)
- # is 63 characters. CI_ENVIRONMENT_SLUG is limited to 24 characters.
- ENV["CI_ENVIRONMENT_SLUG"]
+ "docs-preview-#{slug}-#{ENV["CI_MERGE_REQUEST_IID"]}"
end
#
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
new file mode 100644
index 00000000000..49ed796d9a8
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
@@ -0,0 +1,121 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
+import createStore from '~/vue_merge_request_widget/stores/artifacts_list';
+import { artifactsList } from './mock_data';
+
+describe('Merge Requests Artifacts list app', () => {
+ let wrapper;
+ let mock;
+ const store = createStore();
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const actionSpies = {
+ fetchArtifacts: jest.fn(),
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const createComponent = () => {
+ wrapper = mount(localVue.extend(ArtifactsListApp), {
+ propsData: {
+ endpoint: TEST_HOST,
+ },
+ store,
+ methods: {
+ ...actionSpies,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ const findButtons = () => wrapper.findAll('button');
+ const findTitle = () => wrapper.find('.js-title');
+ const findTableRows = () => wrapper.findAll('tbody tr');
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent();
+ store.dispatch('requestArtifacts');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders a loading icon', () => {
+ const loadingIcon = wrapper.find(GlLoadingIcon);
+ expect(loadingIcon.exists()).toBe(true);
+ });
+
+ it('renders loading text', () => {
+ expect(findTitle().text()).toBe('Loading artifacts');
+ });
+
+ it('renders disabled buttons', () => {
+ const buttons = findButtons();
+ expect(buttons.at(0).attributes('disabled')).toBe('disabled');
+ expect(buttons.at(1).attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('with results', () => {
+ beforeEach(() => {
+ createComponent();
+ mock.onGet(wrapper.vm.$store.state.endpoint).reply(200, artifactsList, {});
+ store.dispatch('receiveArtifactsSuccess', {
+ data: artifactsList,
+ status: 200,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders a title with the number of artifacts', () => {
+ expect(findTitle().text()).toBe('View 2 exposed artifacts');
+ });
+
+ it('renders both buttons enabled', () => {
+ const buttons = findButtons();
+ expect(buttons.at(0).attributes('disabled')).toBe(undefined);
+ expect(buttons.at(1).attributes('disabled')).toBe(undefined);
+ });
+
+ describe('on click', () => {
+ it('renders the list of artifacts', () => {
+ findTitle().trigger('click');
+ wrapper.vm.$nextTick(() => {
+ expect(findTableRows().length).toEqual(2);
+ });
+ });
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ createComponent();
+ mock.onGet(wrapper.vm.$store.state.endpoint).reply(500, {}, {});
+ store.dispatch('receiveArtifactsError');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders the error state', () => {
+ expect(findTitle().text()).toBe('An error occurred while fetching the artifacts');
+ });
+
+ it('renders disabled buttons', () => {
+ const buttons = findButtons();
+ expect(buttons.at(0).attributes('disabled')).toBe('disabled');
+ expect(buttons.at(1).attributes('disabled')).toBe('disabled');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
new file mode 100644
index 00000000000..8c805faf574
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
@@ -0,0 +1,61 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import ArtifactsList from '~/vue_merge_request_widget/components/artifacts_list.vue';
+import { artifactsList } from './mock_data';
+
+describe('Artifacts List', () => {
+ let wrapper;
+ const localVue = createLocalVue();
+
+ const data = {
+ artifacts: artifactsList,
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(localVue.extend(ArtifactsList), {
+ propsData: {
+ ...props,
+ },
+ sync: false,
+ localVue,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ mountComponent(data);
+ });
+
+ it('renders list of artifacts', () => {
+ expect(wrapper.findAll('tbody tr').length).toEqual(data.artifacts.length);
+ });
+
+ it('renders link for the artifact', () => {
+ expect(wrapper.find(GlLink).attributes('href')).toEqual(data.artifacts[0].url);
+ });
+
+ it('renders artifact name', () => {
+ expect(wrapper.find(GlLink).text()).toEqual(data.artifacts[0].text);
+ });
+
+ it('renders job url', () => {
+ expect(
+ wrapper
+ .findAll(GlLink)
+ .at(1)
+ .attributes('href'),
+ ).toEqual(data.artifacts[0].job_path);
+ });
+
+ it('renders job name', () => {
+ expect(
+ wrapper
+ .findAll(GlLink)
+ .at(1)
+ .text(),
+ ).toEqual(data.artifacts[0].job_name);
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mock_data.js b/spec/frontend/vue_mr_widget/components/mock_data.js
new file mode 100644
index 00000000000..39c7d75cda5
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mock_data.js
@@ -0,0 +1,15 @@
+// eslint-disable-next-line import/prefer-default-export
+export const artifactsList = [
+ {
+ text: 'result.txt',
+ url: 'bar',
+ job_name: 'generate-artifact',
+ job_path: 'bar',
+ },
+ {
+ text: 'foo.txt',
+ url: 'foo',
+ job_name: 'foo-artifact',
+ job_path: 'foo',
+ },
+];
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
new file mode 100644
index 00000000000..4c9507223a1
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -0,0 +1,105 @@
+import { mount } from '@vue/test-utils';
+import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+describe('Merge Request Collapsible Extension', () => {
+ let wrapper;
+ const data = {
+ title: 'View artifacts',
+ };
+
+ const mountComponent = props => {
+ wrapper = mount(MrCollapsibleSection, {
+ propsData: {
+ ...props,
+ },
+ slots: {
+ default: '<div class="js-slot">Foo</div>',
+ },
+ });
+ };
+
+ const findTitle = () => wrapper.find('.js-title');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while collapsed', () => {
+ beforeEach(() => {
+ mountComponent(data);
+ });
+
+ it('renders provided title', () => {
+ expect(findTitle().text()).toBe(data.title);
+ });
+
+ it('renders angle-right icon', () => {
+ expect(wrapper.vm.arrowIconName).toBe('angle-right');
+ });
+
+ describe('onClick', () => {
+ beforeEach(() => {
+ wrapper.find('button').trigger('click');
+ });
+
+ it('rendes the provided slot', () => {
+ expect(wrapper.find('.js-slot').isVisible()).toBe(true);
+ });
+
+ it('renders `Collapse` as the title', () => {
+ expect(findTitle().text()).toBe('Collapse');
+ });
+
+ it('renders angle-down icon', () => {
+ expect(wrapper.vm.arrowIconName).toBe('angle-down');
+ });
+ });
+ });
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ mountComponent(Object.assign({}, data, { isLoading: true }));
+ });
+
+ it('renders the buttons disabled', () => {
+ expect(
+ wrapper
+ .findAll('button')
+ .at(0)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ expect(
+ wrapper
+ .findAll('button')
+ .at(1)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ });
+
+ it('renders loading spinner', () => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ mountComponent(Object.assign({}, data, { hasError: true }));
+ });
+
+ it('renders the buttons disabled', () => {
+ expect(
+ wrapper
+ .findAll('button')
+ .at(0)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ expect(
+ wrapper
+ .findAll('button')
+ .at(1)
+ .attributes('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js
new file mode 100644
index 00000000000..62ee6f5f189
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js
@@ -0,0 +1,32 @@
+import { title } from '~/vue_merge_request_widget/stores/artifacts_list/getters';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import { artifactsList } from '../../components/mock_data';
+
+describe('Artifacts Store Getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('title', () => {
+ describe('when is loading', () => {
+ it('returns loading message', () => {
+ localState.isLoading = true;
+ expect(title(localState)).toBe('Loading artifacts');
+ });
+ });
+ describe('when has error', () => {
+ it('returns error message', () => {
+ localState.hasError = true;
+ expect(title(localState)).toBe('An error occurred while fetching the artifacts');
+ });
+ });
+ describe('when it has artifacts', () => {
+ it('returns artifacts message', () => {
+ localState.artifacts = artifactsList;
+ expect(title(localState)).toBe('View 2 exposed artifacts');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
new file mode 100644
index 00000000000..ea89fdb72e9
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
@@ -0,0 +1,78 @@
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations';
+import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+
+describe('Artifacts Store Mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_ENDPOINT', () => {
+ it('should set endpoint', () => {
+ mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
+
+ expect(stateCopy.endpoint).toEqual('endpoint.json');
+ });
+ });
+
+ describe('REQUEST_ARTIFACTS', () => {
+ it('should set isLoading to true', () => {
+ mutations[types.REQUEST_ARTIFACTS](stateCopy);
+
+ expect(stateCopy.isLoading).toEqual(true);
+ });
+ });
+
+ describe('REECEIVE_ARTIFACTS_SUCCESS', () => {
+ const artifacts = [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ {
+ text: 'file.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ];
+
+ beforeEach(() => {
+ mutations[types.RECEIVE_ARTIFACTS_SUCCESS](stateCopy, artifacts);
+ });
+
+ it('should set isLoading to false', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to false', () => {
+ expect(stateCopy.hasError).toEqual(false);
+ });
+
+ it('should set list of artifacts', () => {
+ expect(stateCopy.artifacts).toEqual(artifacts);
+ });
+ });
+
+ describe('RECEIVE_ARTIFACTS_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_ARTIFACTS_ERROR](stateCopy);
+ });
+
+ it('should set isLoading to false', () => {
+ expect(stateCopy.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to true', () => {
+ expect(stateCopy.hasError).toEqual(true);
+ });
+
+ it('should set list of artifacts as empty array', () => {
+ expect(stateCopy.artifacts).toEqual([]);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index dfbc68c48b9..6cdf60f3535 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,6 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { mockStore } from '../mock_data';
describe('MrWidgetPipelineContainer', () => {
@@ -87,4 +88,10 @@ describe('MrWidgetPipelineContainer', () => {
expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
});
});
+
+ describe('with artifacts path', () => {
+ it('renders the artifacts app', () => {
+ expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 2f79806652b..089ec08fbf9 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -289,4 +289,5 @@ export const mockStore = {
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
+ exposedArtifactsPath: 'exposed_artifacts.json',
};
diff --git a/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
new file mode 100644
index 00000000000..4c4bebcb4cd
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
@@ -0,0 +1,165 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setEndpoint,
+ requestArtifacts,
+ clearEtagPoll,
+ stopPolling,
+ fetchArtifacts,
+ receiveArtifactsSuccess,
+ receiveArtifactsError,
+} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('Artifacts App Store Actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setEndpoint', () => {
+ it('should commit SET_ENDPOINT mutation', done => {
+ testAction(
+ setEndpoint,
+ 'endpoint.json',
+ mockedState,
+ [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestArtifacts', () => {
+ it('should commit REQUEST_ARTIFACTS mutation', done => {
+ testAction(
+ requestArtifacts,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_ARTIFACTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchArtifacts', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ]);
+
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ payload: {
+ data: [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ],
+ status: 200,
+ },
+ type: 'receiveArtifactsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestArtifacts and receiveArtifactsError ', done => {
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ type: 'receiveArtifactsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveArtifactsSuccess', () => {
+ it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 200 },
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
+ [],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 204 },
+ mockedState,
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveArtifactsError', () => {
+ it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => {
+ testAction(
+ receiveArtifactsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});