From 452202e36d3e20755b099a718a92d3f7b80fabb8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 12 Jun 2017 09:20:19 +0000 Subject: Improve Job detail view to make it refreshed in real-time instead of reloading --- app/assets/javascripts/build.js | 31 ++++- app/assets/javascripts/dispatcher.js | 4 - app/assets/javascripts/jobs/components/header.vue | 83 ++++++++++++ .../jobs/components/sidebar_detail_row.vue | 31 +++++ .../jobs/components/sidebar_details_block.vue | 150 +++++++++++++++++++++ app/assets/javascripts/jobs/job_details_bundle.js | 68 ++++++++++ .../javascripts/jobs/job_details_mediator.js | 67 +++++++++ .../javascripts/jobs/services/job_service.js | 14 ++ app/assets/javascripts/jobs/stores/job_store.js | 11 ++ .../javascripts/lib/utils/datetime_utility.js | 21 +++ .../pipelines/components/header_component.vue | 2 +- .../vue_shared/components/header_ci_component.vue | 32 ++++- 12 files changed, 500 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/jobs/components/header.vue create mode 100644 app/assets/javascripts/jobs/components/sidebar_detail_row.vue create mode 100644 app/assets/javascripts/jobs/components/sidebar_details_block.vue create mode 100644 app/assets/javascripts/jobs/job_details_bundle.js create mode 100644 app/assets/javascripts/jobs/job_details_mediator.js create mode 100644 app/assets/javascripts/jobs/services/job_service.js create mode 100644 app/assets/javascripts/jobs/stores/job_store.js (limited to 'app/assets/javascripts') diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index d80b7f5bd42..c28f6e151a0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -149,27 +149,34 @@ window.Build = (function () { Build.prototype.verifyTopPosition = function () { const $buildPage = $('.build-page'); + const $flashError = $('.alert-wrapper'); const $header = $('.build-header', $buildPage); const $runnersStuck = $('.js-build-stuck', $buildPage); const $startsEnvironment = $('.js-environment-container', $buildPage); const $erased = $('.js-build-erased', $buildPage); + const prependTopDefault = 20; + // header + navigation + margin let topPostion = 168; - if ($header) { + if ($header.length) { topPostion += $header.outerHeight(); } - if ($runnersStuck) { + if ($runnersStuck.length) { topPostion += $runnersStuck.outerHeight(); } - if ($startsEnvironment) { - topPostion += $startsEnvironment.outerHeight(); + if ($startsEnvironment.length) { + topPostion += $startsEnvironment.outerHeight() + prependTopDefault; } - if ($erased) { - topPostion += $erased.outerHeight() + 10; + if ($erased.length) { + topPostion += $erased.outerHeight() + prependTopDefault; + } + + if ($flashError.length) { + topPostion += $flashError.outerHeight(); } this.$buildTrace.css({ @@ -245,6 +252,7 @@ window.Build = (function () { Build.prototype.toggleSidebar = function (shouldHide) { const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; + const $toggleButton = $('.js-sidebar-build-toggle-header'); this.$buildTrace .toggleClass('sidebar-expanded', shouldShow) @@ -252,6 +260,16 @@ window.Build = (function () { this.$sidebar .toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-collapsed', shouldHide); + + $('.js-build-page') + .toggleClass('sidebar-expanded', shouldShow) + .toggleClass('sidebar-collapsed', shouldHide); + + if (this.$sidebar.hasClass('right-sidebar-expanded')) { + $toggleButton.addClass('hidden'); + } else { + $toggleButton.removeClass('hidden'); + } }; Build.prototype.sidebarOnResize = function () { @@ -266,6 +284,7 @@ window.Build = (function () { Build.prototype.sidebarOnClick = function () { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); + this.verifyTopPosition(); }; Build.prototype.updateArtifactRemoveDate = function () { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ca90729c791..5f87a05067b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -2,7 +2,6 @@ /* global UsernameValidator */ /* global ActiveTabMemoizer */ /* global ShortcutsNavigation */ -/* global Build */ /* global IssuableIndex */ /* global ShortcutsIssuable */ /* global ZenMode */ @@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels'; shortcut_handler = new ShortcutsNavigation(); new UsersSelect(); break; - case 'projects:jobs:show': - new Build(); - break; case 'projects:merge_requests:index': case 'projects:issues:index': if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue new file mode 100644 index 00000000000..5b9cf577189 --- /dev/null +++ b/app/assets/javascripts/jobs/components/header.vue @@ -0,0 +1,83 @@ + + diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue new file mode 100644 index 00000000000..ab2bcd728a8 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -0,0 +1,31 @@ + + diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue new file mode 100644 index 00000000000..4223a8fea49 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -0,0 +1,150 @@ + + diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js new file mode 100644 index 00000000000..939d17129de --- /dev/null +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -0,0 +1,68 @@ +/* global Flash */ + +import Vue from 'vue'; +import JobMediator from './job_details_mediator'; +import jobHeader from './components/header.vue'; +import detailsBlock from './components/sidebar_details_block.vue'; + +document.addEventListener('DOMContentLoaded', () => { + const dataset = document.getElementById('js-job-details-vue').dataset; + const mediator = new JobMediator({ endpoint: dataset.endpoint }); + + mediator.fetchJob(); + + // Header + // eslint-disable-next-line no-new + new Vue({ + el: '#js-build-header-vue', + data() { + return { + mediator, + }; + }, + components: { + jobHeader, + }, + mounted() { + this.mediator.initBuildClass(); + }, + updated() { + // Wait for flash message to be appended + Vue.nextTick(() => { + if (this.mediator.build) { + this.mediator.build.verifyTopPosition(); + } + }); + }, + render(createElement) { + return createElement('job-header', { + props: { + isLoading: this.mediator.state.isLoading, + job: this.mediator.store.state.job, + }, + }); + }, + }); + + // Sidebar information block + // eslint-disable-next-line + new Vue({ + el: '#js-details-block-vue', + data() { + return { + mediator, + }; + }, + components: { + detailsBlock, + }, + render(createElement) { + return createElement('details-block', { + props: { + isLoading: this.mediator.state.isLoading, + job: this.mediator.store.state.job, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js new file mode 100644 index 00000000000..063c52fac74 --- /dev/null +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -0,0 +1,67 @@ +/* global Flash */ +/* global Build */ + +import Visibility from 'visibilityjs'; +import Poll from '../lib/utils/poll'; +import JobStore from './stores/job_store'; +import JobService from './services/job_service'; +import '../build'; + +export default class JobMediator { + constructor(options = {}) { + this.options = options; + + this.store = new JobStore(); + this.service = new JobService(options.endpoint); + + this.state = { + isLoading: false, + }; + } + + initBuildClass() { + this.build = new Build(); + } + + fetchJob() { + this.poll = new Poll({ + resource: this.service, + method: 'getJob', + successCallback: this.successCallback.bind(this), + errorCallback: this.errorCallback.bind(this), + }); + + if (!Visibility.hidden()) { + this.state.isLoading = true; + this.poll.makeRequest(); + } else { + this.getJob(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + } + + getJob() { + return this.service.getJob() + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + + successCallback(response) { + const data = response.json(); + this.state.isLoading = false; + this.store.storeJob(data); + } + + errorCallback() { + this.state.isLoading = false; + + return new Flash('An error occurred while fetching the job.'); + } +} diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js new file mode 100644 index 00000000000..eaf1c6e500a --- /dev/null +++ b/app/assets/javascripts/jobs/services/job_service.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class JobService { + constructor(endpoint) { + this.job = Vue.resource(endpoint); + } + + getJob() { + return this.job.get(); + } +} diff --git a/app/assets/javascripts/jobs/stores/job_store.js b/app/assets/javascripts/jobs/stores/job_store.js new file mode 100644 index 00000000000..766194b8387 --- /dev/null +++ b/app/assets/javascripts/jobs/stores/job_store.js @@ -0,0 +1,11 @@ +export default class JobStore { + constructor() { + this.state = { + job: {}, + }; + } + + storeJob(job = {}) { + this.state.job = job; + } +} diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 40eadd9396c..54c0da3fc9c 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -146,3 +146,24 @@ window.dateFormat = dateFormat; }; })(window); }).call(window); + +/** + * Port of ruby helper time_interval_in_words. + * + * @param {Number} seconds + * @return {String} + */ +// eslint-disable-next-line import/prefer-default-export +export function timeIntervalInWords(intervalInSeconds) { + const secondsInteger = parseInt(intervalInSeconds, 10); + const minutes = Math.floor(secondsInteger / 60); + const seconds = secondsInteger - (minutes * 60); + let text = ''; + + if (minutes >= 1) { + text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; + } else { + text = `${seconds} ${gl.text.pluralize('second', seconds)}`; + } + return text; +} diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 4f6c5c177cf..2a1ecac3707 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -91,7 +91,7 @@ export default { @actionClicked="postAction" /> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index fe6d6a792e7..1d4d90f75b6 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -40,6 +40,11 @@ export default { required: false, default: () => [], }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, }, mixins: [ @@ -66,8 +71,9 @@ export default { }, }; + -- cgit v1.2.3