summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2017-05-26 17:32:31 +0000
committerPhil Hughes <me@iamphill.com>2017-05-26 17:32:31 +0000
commit7911f1c0877247f943d2c5dc1cb26de19fe69c8c (patch)
treea75cfbf645a4246d032dbb7fbded8e0c22749627
parentfe008c521d777ecfc924929ed373098c24af84c1 (diff)
parentf4274ca1b1ed99523aff95bd78cb74be972dd261 (diff)
downloadgitlab-ce-7911f1c0877247f943d2c5dc1cb26de19fe69c8c.tar.gz
Merge branch 'ci-build-pipeline-header-vue' into 'master'
Creates CI Header component to use in pipelines and job details pages See merge request !11726
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue58
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js18
-rw-r--r--changelogs/unreleased/ci-build-pipeline-header-vue.yml4
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js82
-rw-r--r--spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js68
6 files changed, 352 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
new file mode 100644
index 00000000000..fd0dcd716d6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -0,0 +1,122 @@
+<script>
+import ciIconBadge from './ci_badge_link.vue';
+import timeagoTooltip from './time_ago_tooltip.vue';
+import tooltipMixin from '../mixins/tooltip';
+import userAvatarLink from './user_avatar/user_avatar_link.vue';
+
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ components: {
+ ciIconBadge,
+ timeagoTooltip,
+ userAvatarLink,
+ },
+
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
+ },
+ },
+
+ methods: {
+ onClickAction(action) {
+ this.$emit('postAction', action);
+ },
+ },
+};
+</script>
+<template>
+ <header class="page-content-header top-area">
+ <section class="header-main-content">
+
+ <ci-icon-badge :status="status" />
+
+ <strong>
+ {{itemName}} #{{itemId}}
+ </strong>
+
+ triggered
+
+ <timeago-tooltip :time="time" />
+
+ by
+
+ <user-avatar-link
+ :link-href="user.web_url"
+ :img-src="user.avatar_url"
+ :img-alt="userAvatarAltText"
+ :tooltip-text="user.name"
+ :img-size="24"
+ />
+
+ <a
+ :href="user.web_url"
+ :title="user.email"
+ class="js-user-link commit-committer-link"
+ ref="tooltip">
+ {{user.name}}
+ </a>
+ </section>
+
+ <section
+ class="header-action-button nav-controls"
+ v-if="actions.length">
+ <template
+ v-for="action in actions">
+ <a
+ v-if="action.type === 'link'"
+ :href="action.path"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
+ <button
+ v-else="action.type === 'button'"
+ @click="onClickAction(action)"
+ :class="action.cssClass"
+ type="button">
+ {{action.label}}
+ </button>
+
+ </template>
+ </section>
+ </header>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
new file mode 100644
index 00000000000..af2b4c6786e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -0,0 +1,58 @@
+<script>
+import tooltipMixin from '../mixins/tooltip';
+import timeagoMixin from '../mixins/timeago';
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Port of ruby helper time_ago_with_tooltip
+ */
+
+export default {
+ props: {
+ time: {
+ type: String,
+ required: true,
+ },
+
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+
+ shortFormat: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ timeagoMixin,
+ ],
+
+ computed: {
+ timeagoCssClass() {
+ return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
+ },
+ },
+};
+</script>
+<template>
+ <time
+ :class="[timeagoCssClass, cssClass]"
+ class="js-timeago js-timeago-render"
+ :title="tooltipTitle(time)"
+ :data-placement="tooltipPlacement"
+ data-container="body"
+ ref="tooltip">
+ {{timeFormated(time)}}
+ </time>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
new file mode 100644
index 00000000000..20f63ab663c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -0,0 +1,18 @@
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Mixin with time ago methods used in some vue components
+ */
+export default {
+ methods: {
+ timeFormated(time) {
+ const timeago = gl.utils.getTimeago();
+
+ return timeago.format(time);
+ },
+
+ tooltipTitle(time) {
+ return gl.utils.formatDate(time);
+ },
+ },
+};
diff --git a/changelogs/unreleased/ci-build-pipeline-header-vue.yml b/changelogs/unreleased/ci-build-pipeline-header-vue.yml
new file mode 100644
index 00000000000..2bbff2fdd16
--- /dev/null
+++ b/changelogs/unreleased/ci-build-pipeline-header-vue.yml
@@ -0,0 +1,4 @@
+---
+title: Creates CI Header component for Pipelines and Jobs details pages
+merge_request:
+author:
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
new file mode 100644
index 00000000000..1bf8916b3d0
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
+
+describe('Header CI Component', () => {
+ let HeaderCi;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderCi = Vue.extend(headerCi);
+
+ props = {
+ status: {
+ group: 'failed',
+ icon: 'ci-status-failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ actions: [
+ {
+ label: 'Retry',
+ path: 'path',
+ type: 'button',
+ cssClass: 'btn',
+ },
+ {
+ label: 'Go',
+ path: 'path',
+ type: 'link',
+ cssClass: 'link',
+ },
+ ],
+ };
+
+ vm = new HeaderCi({
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.ci-failed').getAttribute('href'),
+ ).toEqual(props.status.details_path);
+ });
+
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
+
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
+
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ });
+
+ it('should render provided actions', () => {
+ expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
+ expect(vm.$el.querySelector('.link').tagName).toEqual('A');
+ expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
+ expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
new file mode 100644
index 00000000000..bf28019ef24
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import '~/lib/utils/datetime_utility';
+
+describe('Time ago with tooltip component', () => {
+ let TimeagoTooltip;
+ let vm;
+
+ beforeEach(() => {
+ TimeagoTooltip = Vue.extend(timeagoTooltip);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render timeago with a bootstrap tooltip', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ },
+ }).$mount();
+
+ expect(vm.$el.tagName).toEqual('TIME');
+ expect(vm.$el.classList.contains('js-timeago')).toEqual(true);
+ expect(
+ vm.$el.getAttribute('data-original-title'),
+ ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
+ expect(vm.$el.getAttribute('data-placement')).toEqual('top');
+
+ const timeago = gl.utils.getTimeago();
+
+ expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z'));
+ });
+
+ it('should render tooltip placed in bottom', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ tooltipPlacement: 'bottom',
+ },
+ }).$mount();
+
+ expect(vm.$el.getAttribute('data-placement')).toEqual('bottom');
+ });
+
+ it('should render short format class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ shortFormat: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true);
+ });
+
+ it('should render provided html class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ cssClass: 'foo',
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('foo')).toEqual(true);
+ });
+});