Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-06-12 12:20:19 +0300
committerPhil Hughes <me@iamphill.com>2017-06-12 12:20:19 +0300
commit452202e36d3e20755b099a718a92d3f7b80fabb8 (patch)
tree5cc8bd9c5d810a645f44f515c4310731fa01785f /app/assets/javascripts
parentd25f6fcf629bd773ccac49a799393479c48f4673 (diff)
Improve Job detail view to make it refreshed in real-time instead of reloading
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/build.js31
-rw-r--r--app/assets/javascripts/dispatcher.js4
-rw-r--r--app/assets/javascripts/jobs/components/header.vue83
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue31
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue150
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js68
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js67
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js14
-rw-r--r--app/assets/javascripts/jobs/stores/job_store.js11
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js21
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue32
12 files changed, 500 insertions, 14 deletions
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 @@
+<script>
+ import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+ export default {
+ name: 'jobHeaderSection',
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ ciHeader,
+ loadingIcon,
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
+ },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
+
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ type: 'ujs-link',
+ });
+ }
+
+ if (this.job.retry_path) {
+ actions.push({
+ label: 'Retry',
+ path: this.job.retry_path,
+ cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
+ type: 'ujs-link',
+ });
+ }
+
+ return actions;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ };
+</script>
+<template>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :hasSidebarButton="true"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ />
+ </div>
+</template>
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 @@
+<script>
+ export default {
+ name: 'SidebarDetailRow',
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ hasTitle() {
+ return this.title.length > 0;
+ },
+ },
+ };
+</script>
+<template>
+ <p class="build-detail-row">
+ <span
+ v-if="hasTitle"
+ class="build-light-text">
+ {{title}}:
+ </span>
+ {{value}}
+ </p>
+</template>
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 @@
+<script>
+ import detailRow from './sidebar_detail_row.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import timeagoMixin from '../../vue_shared/mixins/timeago';
+ import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+
+ export default {
+ name: 'SidebarDetailsBlock',
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `#${this.job.runner.id}`;
+ },
+ },
+ };
+</script>
+<template>
+ <div>
+ <template v-if="shouldRenderContent">
+ <div
+ class="block retry-link"
+ v-if="job.retry_path || job.new_issue_path">
+ <a
+ v-if="job.new_issue_path"
+ class="js-new-issue btn btn-new btn-inverted"
+ :href="job.new_issue_path">
+ New issue
+ </a>
+ <a
+ v-if="job.retry_path"
+ class="js-retry-job btn btn-inverted-secondary"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow">
+ Retry
+ </a>
+ </div>
+ <div class="block">
+ <p
+ class="build-detail-row js-job-mr"
+ v-if="job.merge_request">
+ <span
+ class="build-light-text">
+ Merge Request:
+ </span>
+ <a :href="job.merge_request.path">
+ !{{job.merge_request.iid}}
+ </a>
+ </p>
+
+ <detail-row
+ class="js-job-duration"
+ v-if="job.duration"
+ title="Duration"
+ :value="duration"
+ />
+ <detail-row
+ class="js-job-finished"
+ v-if="job.finished_at"
+ title="Finished"
+ :value="timeFormated(job.finished_at)"
+ />
+ <detail-row
+ class="js-job-erased"
+ v-if="job.erased_at"
+ title="Erased"
+ :value="timeFormated(job.erased_at)"
+ />
+ <detail-row
+ class="js-job-queued"
+ v-if="job.queued"
+ title="Queued"
+ :value="queued"
+ />
+ <detail-row
+ class="js-job-runner"
+ v-if="job.runner"
+ title="Runner"
+ :value="runnerId"
+ />
+ <detail-row
+ class="js-job-coverage"
+ v-if="job.coverage"
+ title="Coverage"
+ :value="coverage"
+ />
+ <p
+ class="build-detail-row js-job-tags"
+ v-if="job.tags.length">
+ <span
+ class="build-light-text">
+ Tags:
+ </span>
+ <span
+ v-for="tag in job.tags"
+ key="tag"
+ class="label label-primary">
+ {{tag}}
+ </span>
+ </p>
+
+ <div
+ v-if="job.cancel_path"
+ class="btn-group prepend-top-5"
+ role="group">
+ <a
+ class="js-cancel-job btn btn-sm btn-default"
+ :href="job.cancel_path"
+ data-method="post"
+ rel="nofollow">
+ Cancel
+ </a>
+ </div>
+ </div>
+ </template>
+ <loading-icon
+ class="prepend-top-10"
+ v-if="isLoading"
+ size="2"
+ />
+ </div>
+</template>
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"
/>
<loading-icon
- v-else
+ v-if="isLoading"
size="2"/>
</div>
</template>
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 {
},
};
</script>
+
<template>
- <header class="page-content-header">
+ <header class="page-content-header ci-header-container">
<section class="header-main-content">
<ci-icon-badge :status="status" />
@@ -102,7 +108,7 @@ export default {
</section>
<section
- class="header-action-button nav-controls"
+ class="header-action-buttons"
v-if="actions.length">
<template
v-for="action in actions">
@@ -113,6 +119,15 @@ export default {
{{action.label}}
</a>
+ <a
+ v-if="action.type === 'ujs-link'"
+ :href="action.path"
+ data-method="post"
+ rel="nofollow"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
@@ -120,7 +135,6 @@ export default {
:class="action.cssClass"
type="button">
{{action.label}}
-
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
@@ -128,6 +142,18 @@ export default {
</i>
</button>
</template>
+ <button
+ v-if="hasSidebarButton"
+ type="button"
+ class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
+ aria-label="Toggle Sidebar"
+ id="toggleSidebar">
+ <i
+ class="fa fa-angle-double-left"
+ aria-hidden="true"
+ aria-labelledby="toggleSidebar">
+ </i>
+ </button>
</section>
</header>
</template>