Commit 79dc74b0 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'vue-pipelines-code' into 'master'

Move vue components to vue files and match docs

See merge request !12146
parents d6fee794 8868efd0
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store'; import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub'; import eventHub from '../../pipelines/event_hub';
......
...@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue'; ...@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
......
<script>
import Visibility from 'visibilityjs';
import PipelinesService from '../services/pipelines_service';
import eventHub from '../event_hub';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import emptyState from './empty_state.vue';
import errorState from './error_state.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Poll from '../../lib/utils/poll';
export default {
props: {
store: {
type: Object,
required: true,
},
},
components: {
tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs,
navigationControls,
loadingIcon,
},
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers,
body: resp.json(),
};
this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
};
</script>
<template>
<div :class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true">
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true">
</i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
<div class="content-list pipelines">
<loading-icon
label="Loading Pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
/>
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
/>
</div>
</div>
</template>
/* eslint-disable no-new */
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIconComponent from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
loadingIconComponent,
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
<div class="btn-group" v-if="actions">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
${playIconSvg}
<i
class="fa fa-caret-down"
aria-hidden="true" />
<loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
`,
};
<script>
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
loadingIcon,
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
};
</script>
<template>
<div class="btn-group">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
<loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
<span v-html="playIconSvg"></span>
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
</template>
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
template: `
<div class="btn-group" role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
download
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
`,
};
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
mixins: [
tooltipMixin,
],
};
</script>
<template>
<div
class="btn-group"
role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
<i
class="fa fa-download"
aria-hidden="true">
</i>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
download
:href="artifact.path">
<i
class="fa fa-download"
aria-hidden="true">
</i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
</template>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() {
return {
iconTimerSvg,
};
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: {
hasDuration() {
return this.duration > 0;
},
hasFinishedTime() {
return this.finishedTime !== '';
},
localTimeFinished() {
return gl.utils.formatDate(this.finishedTime);
},
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
},
finishedTimeFormated() {
const timeAgo = gl.utils.getTimeago();
return timeAgo.format(this.finishedTime);
},
},
template: `
<td class="pipelines-time-ago">
<p
class="duration"
v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p>
<p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true" />
<time
ref="tooltip"
data-toggle="tooltip"
data-placement="top"
data-container="body"
:title="localTimeFinished">
{{finishedTimeFormated}}
</time>
</p>
</td>
`,
};
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
data() {
return {
iconTimerSvg,
};
},
computed: {
hasDuration() {
return this.duration > 0;
},
hasFinishedTime() {
return this.finishedTime !== '';
},
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
},
},
};
</script>
<template>
<td class="pipelines-time-ago">
<p
class="duration"
v-if="hasDuration">
<span v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p>
<p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true">
</i>
<time
ref="tooltip"
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">
{{timeFormated(finishedTime)}}
</time>
</p>
</td>
</script>
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
import tablePagination from '../vue_shared/components/table_pagination.vue';
import emptyState from './components/empty_state.vue';
import errorState from './components/error_state.vue';
import navigationTabs from './components/navigation_tabs.vue';
import navigationControls from './components/nav_controls.vue';
import loadingIcon from '../vue_shared/components/loading_icon.vue';
import Poll from '../lib/utils/poll';
export default {
props: {
store: {
type: Object,
required: true,
},
},
components: {
tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs,
navigationControls,
loadingIcon,
},
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers,
body: resp.json(),
};
this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
template: `
<div :class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
<div class="fade-left">
<i class="fa fa-angle-left" aria-hidden="true"></i>
</div>
<div class="fade-right">
<i class="fa fa-angle-right" aria-hidden="true"></i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths" />
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " />
</div>
<div class="content-list pipelines">
<loading-icon
label="Loading Pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
/>
</div>
</div>
`,
};
import Vue from 'vue'; import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store'; import PipelinesStore from './stores/pipelines_store';
import PipelinesComponent from './pipelines'; import pipelinesComponent from './components/pipelines.vue';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
el: document.querySelector('#pipelines-list-vue'),
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipelines-list-vue',
data() { data() {
const store = new PipelinesStore(); const store = new PipelinesStore();
...@@ -14,9 +12,13 @@ $(() => new Vue({ ...@@ -14,9 +12,13 @@ $(() => new Vue({
}; };
}, },
components: { components: {
'vue-pipelines': PipelinesComponent, pipelinesComponent,
},
render(createElement) {
return createElement('pipelines-component', {
props: {
store: this.store,
},
});
}, },
template: `
<vue-pipelines :store="store" />
`,
})); }));
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render `fa-code-fork` icon.
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
},
components: {
userAvatarLink,
},
template: `
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container">
<i v-if="tag" class="fa fa-tag"></i>
<i v-if="!tag" class="fa fa-code-fork"></i>
</div>
<a v-if="hasCommitRef"
class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-sha"
:href="commitUrl">
{{shortSha}}
</a>
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
</span>
</div>
</div>
`,
};
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render `fa-code-fork` icon.
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
},
components: {
userAvatarLink,
},
};
</script>
<template>
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container">
<i
v-if="tag"
class="fa fa-tag"
aria-hidden="true">
</i>
<i
v-if="!tag"
class="fa fa-code-fork"
aria-hidden="true">
</i>
</div>
<a
v-if="hasCommitRef"
class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div
v-html="commitIconSvg"
class="commit-icon js-commit-icon">
</div>
<a
class="commit-sha"
:href="commitUrl">
{{shortSha}}
</a>
<div class="commit-title flex-truncate-parent">
<span
v-if="title"
class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
</span>
</div>
</div>
</template>
import PipelinesTableRowComponent from './pipelines_table_row';
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
export default {
props: {
pipelines: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
'pipelines-table-row-component': PipelinesTableRowComponent,
},
template: `
<table class="table ci-table">
<thead>
<tr>
<th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in pipelines"
v-bind:model="model">
<tr is="pipelines-table-row-component"
:pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
`,
};
<script>
import pipelinesTableRowComponent from './pipelines_table_row.vue';
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
export default {
props: {
pipelines: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
pipelinesTableRowComponent,
},
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in pipelines"
:model="model">
<tr
is="pipelines-table-row-component"
:pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
</template>
<script>
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import asyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue';
import ciBadge from './ci_badge_link.vue'; import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue'; import pipelineStage from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue'; import pipelineUrl from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import pipelinesTimeago from '../../pipelines/components/time_ago.vue';
import CommitComponent from './commit'; import commitComponent from './commit.vue';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -19,30 +20,26 @@ export default { ...@@ -19,30 +20,26 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
service: { service: {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
}, },
components: { components: {
'async-button-component': AsyncButtonComponent, asyncButtonComponent,
'pipelines-actions-component': PipelinesActionsComponent, pipelinesActionsComponent,
'pipelines-artifacts-component': PipelinesArtifactsComponent, pipelinesArtifactsComponent,
'commit-component': CommitComponent, commitComponent,
'dropdown-stage': PipelinesStageComponent, pipelineStage,
'pipeline-url': PipelinesUrlComponent, pipelineUrl,
ciBadge, ciBadge,
'time-ago': PipelinesTimeagoComponent, pipelinesTimeago,
}, },
computed: { computed: {
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
...@@ -204,69 +201,76 @@ export default { ...@@ -204,69 +201,76 @@ export default {
return {}; return {};
}, },
}, },
};
</script>
<template>
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus" />
</td>
template: ` <pipeline-url :pipeline="pipeline" />
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url>
<td> <td>
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit-ref="commitRef" :commit-ref="commitRef"
:commit-url="commitUrl" :commit-url="commitUrl"
:short-sha="commitShortSha" :short-sha="commitShortSha"
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"/> :author="commitAuthor"
</td> />
</td>
<td class="stage-cell"> <td class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph" <div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0" v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages"> v-for="stage in pipeline.details.stages">
<dropdown-stage <pipeline-stage
:stage="stage" :stage="stage"
:update-dropdown="updateGraphDropdown"/> :update-dropdown="updateGraphDropdown"
</div> />
</td> </div>
</td>
<time-ago <pipelines-timeago
:duration="pipelineDuration" :duration="pipelineDuration"
:finished-time="pipelineFinishedAt" /> :finished-time="pipelineFinishedAt"
/>
<td class="pipeline-actions"> <td class="pipeline-actions">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions" :actions="pipeline.details.manual_actions"
:service="service" /> :service="service"
/>
<pipelines-artifacts-component <pipelines-artifacts-component
v-if="pipeline.details.artifacts.length" v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts" /> :artifacts="pipeline.details.artifacts"
/>
<async-button-component <async-button-component
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:service="service" :service="service"
:endpoint="pipeline.retry_path" :endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" /> icon="repeat"
/>
<async-button-component <async-button-component
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:service="service" :service="service"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
icon="remove" icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?" /> confirm-action-message="Are you sure you want to cancel this pipeline?"
</div> />
</td> </div>
</tr> </td>
`, </tr>
}; </tr>
...@@ -61,7 +61,7 @@ var config = { ...@@ -61,7 +61,7 @@ var config = {
network: './network/network_bundle.js', network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/index.js', pipelines: './pipelines/pipelines_bundle.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesActionsComp from '~/pipelines/components/pipelines_actions'; import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue';
describe('Pipelines Actions dropdown', () => { describe('Pipelines Actions dropdown', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import artifactsComp from '~/pipelines/components/pipelines_artifacts'; import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => { describe('Pipelines Artifacts dropdown', () => {
let component; let component;
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesComp from '~/pipelines/pipelines'; import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store'; import Store from '~/pipelines/stores/pipelines_store';
describe('Pipelines', () => { describe('Pipelines', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import timeAgo from '~/pipelines/components/time_ago'; import timeAgo from '~/pipelines/components/time_ago.vue';
describe('Timeago component', () => { describe('Timeago component', () => {
let TimeAgo; let TimeAgo;
......
import Vue from 'vue'; import Vue from 'vue';
import commitComp from '~/vue_shared/components/commit'; import commitComp from '~/vue_shared/components/commit.vue';
describe('Commit component', () => { describe('Commit component', () => {
let props; let props;
......
import Vue from 'vue'; import Vue from 'vue';
import tableRowComp from '~/vue_shared/components/pipelines_table_row'; import tableRowComp from '~/vue_shared/components/pipelines_table_row.vue';
describe('Pipelines Table Row', () => { describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesTableComp from '~/vue_shared/components/pipelines_table'; import pipelinesTableComp from '~/vue_shared/components/pipelines_table.vue';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
describe('Pipelines Table', () => { describe('Pipelines Table', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment