diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-02-01 16:15:42 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-02-01 16:15:42 +0000 |
commit | 72be759af17c1b93f44caa8eaaae2037ae158c62 (patch) | |
tree | 8bba66b07846b46bd3c02abe66551589923ae68c | |
parent | d974ddb70efea6b58a4ead83a1ac372a913c4985 (diff) | |
parent | 1123d9dc460353cbc3b46606cc2235f0433f35e1 (diff) | |
download | gitlab-ce-72be759af17c1b93f44caa8eaaae2037ae158c62.tar.gz |
Merge branch '35779-realtime-update-of-pipeline-status-in-files-view' into 'master'
Realtime update of pipeline status in Files view
Closes #35779
See merge request gitlab-org/gitlab-ce!16523
7 files changed, 280 insertions, 0 deletions
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 28a0160f47d..c4b3356e478 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,3 +1,5 @@ +import Vue from 'vue'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import TreeView from '../../../../tree'; import ShortcutsNavigation from '../../../../shortcuts_navigation'; import BlobViewer from '../../../../blob/viewer'; @@ -11,5 +13,25 @@ export default () => { new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new $('#tree-slider').waitForImages(() => ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); + + const commitPipelineStatusEl = document.getElementById('commit-pipeline-status'); + const statusLink = document.querySelector('.commit-actions .ci-status-link'); + if (statusLink != null) { + statusLink.remove(); + // eslint-disable-next-line no-new + new Vue({ + el: commitPipelineStatusEl, + components: { + commitPipelineStatus, + }, + render(createElement) { + return createElement('commit-pipeline-status', { + props: { + endpoint: commitPipelineStatusEl.dataset.endpoint, + }, + }); + }, + }); + } }; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue new file mode 100644 index 00000000000..63f20a0041d --- /dev/null +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -0,0 +1,120 @@ +<script> + import Visibility from 'visibilityjs'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import Poll from '~/lib/utils/poll'; + import Flash from '~/flash'; + import { s__, sprintf } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import CommitPipelineService from '../services/commit_pipeline_service'; + + export default { + directives: { + tooltip, + }, + components: { + ciIcon, + loadingIcon, + }, + props: { + endpoint: { + type: String, + required: true, + }, + /* This prop can be used to replace some of the `render_commit_status` + used across GitLab, this way we could use this vue component and add a + realtime status where it makes sense + realtime: { + type: Boolean, + required: false, + default: true, + }, */ + }, + data() { + return { + ciStatus: {}, + isLoading: true, + }; + }, + computed: { + statusTitle() { + return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); + }, + }, + mounted() { + this.service = new CommitPipelineService(this.endpoint); + this.initPolling(); + }, + methods: { + successCallback(res) { + const pipelines = res.data.pipelines; + if (pipelines.length > 0) { + // The pipeline entity always keeps the latest pipeline info on the `details.status` + this.ciStatus = pipelines[0].details.status; + } + this.isLoading = false; + }, + errorCallback() { + this.ciStatus = { + text: 'not found', + icon: 'status_notfound', + group: 'notfound', + }; + this.isLoading = false; + Flash(s__('Something went wrong on our end')); + }, + initPolling() { + this.poll = new Poll({ + resource: this.service, + method: 'fetchData', + successCallback: response => this.successCallback(response), + errorCallback: this.errorCallback, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + this.fetchPipelineCommitData(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + fetchPipelineCommitData() { + this.service.fetchData() + .then(this.successCallback) + .catch(this.errorCallback); + }, + }, + destroy() { + this.poll.stop(); + }, + }; +</script> +<template> + <div> + <loading-icon + label="Loading pipeline status" + size="3" + v-if="isLoading" + /> + <a + v-else + :href="ciStatus.details_path" + > + <ci-icon + v-tooltip + :title="statusTitle" + :aria-label="statusTitle" + data-container="body" + :status="ciStatus" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js new file mode 100644 index 00000000000..4b4189bc2de --- /dev/null +++ b/app/assets/javascripts/projects/tree/services/commit_pipeline_service.js @@ -0,0 +1,11 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class CommitPipelineService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchData() { + return axios.get(this.endpoint); + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index aeaa33bd3bd..17801ed5910 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -195,6 +195,18 @@ .commit-actions { @media (min-width: $screen-sm-min) { font-size: 0; + + div { + display: inline; + } + + .fa-spinner { + font-size: 12px; + } + + span { + font-size: 6px; + } } .ci-status-link { @@ -219,6 +231,11 @@ font-size: 14px; font-weight: $gl-font-weight-bold; } + + .ci-status-icon { + position: relative; + top: 1px; + } } .commit, diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index d66066a6d0b..90272ad9554 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -51,6 +51,7 @@ - if commit.status(ref) = render_commit_status(commit, ref: ref) + #commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } } = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = link_to_browse_code(project, commit) diff --git a/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml new file mode 100644 index 00000000000..82df00fe631 --- /dev/null +++ b/changelogs/unreleased/35779-realtime-update-of-pipeline-status-in-files-view.yml @@ -0,0 +1,5 @@ +--- +title: Add realtime ci status for the repository -> files view +merge_request: 16523 +author: +type: added diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js new file mode 100644 index 00000000000..90f290e845e --- /dev/null +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -0,0 +1,104 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit pipeline status component', () => { + let vm; + let Component; + let mock; + const mockCiStatus = { + details_path: '/root/hello-world/pipelines/1', + favicon: 'canceled.ico', + group: 'canceled', + has_details: true, + icon: 'status_canceled', + label: 'canceled', + text: 'canceled', + }; + + beforeEach(() => { + Component = Vue.extend(commitPipelineStatus); + }); + + describe('While polling pipeline data succesfully', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.resolve([200, { + pipelines: [ + { + details: { + status: mockCiStatus, + }, + }, + ], + }]); + return res; + }); + vm = mountComponent(Component, { + endpoint: '/dummy/endpoint', + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('shows the loading icon when polling is starting', (done) => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + setTimeout(() => { + expect(vm.$el.querySelector('.loading-container')).toBe(null); + done(); + }); + }); + + it('contains a ciStatus when the polling is succesful ', (done) => { + setTimeout(() => { + expect(vm.ciStatus).toEqual(mockCiStatus); + done(); + }); + }); + + it('contains a ci-status icon when polling is succesful', (done) => { + setTimeout(() => { + expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); + expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`); + done(); + }); + }); + }); + + describe('When polling data was not succesful', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/dummy/endpoint').reply(() => { + const res = Promise.reject([502, { }]); + return res; + }); + vm = new Component({ + props: { + endpoint: '/dummy/endpoint', + }, + }); + }); + + afterEach(() => { + vm.poll.stop(); + vm.$destroy(); + mock.restore(); + }); + + it('calls an errorCallback', (done) => { + spyOn(vm, 'errorCallback').and.callThrough(); + vm.$mount(); + setTimeout(() => { + expect(vm.errorCallback.calls.count()).toEqual(1); + done(); + }); + }); + }); +}); |