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
This commit is contained in:
Phil Hughes 2017-05-26 17:32:31 +00:00
commit 7911f1c087
6 changed files with 352 additions and 0 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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);
},
},
};

View file

@ -0,0 +1,4 @@
---
title: Creates CI Header component for Pipelines and Jobs details pages
merge_request:
author:

View file

@ -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);
});
});

View file

@ -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);
});
});