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:
commit
7911f1c087
6 changed files with 352 additions and 0 deletions
|
@ -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>
|
|
@ -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>
|
18
app/assets/javascripts/vue_shared/mixins/timeago.js
Normal file
18
app/assets/javascripts/vue_shared/mixins/timeago.js
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
4
changelogs/unreleased/ci-build-pipeline-header-vue.yml
Normal file
4
changelogs/unreleased/ci-build-pipeline-header-vue.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Creates CI Header component for Pipelines and Jobs details pages
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue