Skip to content
Snippets Groups Projects
Commit a1348a5d authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes
Browse files

Uniform CI status components in vue

parent aa6f4432
No related branches found
No related tags found
No related merge requests found
Showing
with 213 additions and 266 deletions
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
svgHTML() {
return borderlessStatusIconEntityMap[this.stage.status.icon];
},
},
watch: {
'stage.title': function stageTitle() {
$(this.$refs.button).tooltip('destroy').tooltip();
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
ref="button"
:aria-label="stage.title">
<span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
};
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
return `ci-status ci-${this.pipeline.details.status.group}`;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
};
/* global Flash */
 
import '~/lib/utils/datetime_utility';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service';
 
Loading
Loading
@@ -16,7 +16,7 @@ export default {
},
computed: {
svg() {
return statusClassToSvgMap.icon_status_success;
return statusIconEntityMap.icon_status_success;
},
},
methods: {
Loading
Loading
import PipelineStage from '../../pipelines/components/stage';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
 
export default {
name: 'MRWidgetPipeline',
Loading
Loading
@@ -9,7 +9,7 @@ export default {
},
components: {
'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon,
ciIcon,
},
computed: {
hasCIError() {
Loading
Loading
@@ -18,11 +18,14 @@ export default {
return hasCI && !ciStatus;
},
svg() {
return statusClassToSvgMap.icon_status_failed;
return statusIconEntityMap.icon_status_failed;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
status() {
return this.mr.pipeline.details.status || {};
},
},
template: `
<div class="mr-widget-heading">
Loading
Loading
@@ -38,7 +41,13 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" />
<div>
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
<span>
Pipeline
<a
Loading
Loading
Loading
Loading
@@ -41,15 +41,3 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
export const statusCssClasses = {
icon_status_canceled: 'canceled',
icon_status_created: 'created',
icon_status_failed: 'failed',
icon_status_manual: 'manual',
icon_status_pending: 'pending',
icon_status_running: 'running',
icon_status_skipped: 'skipped',
icon_status_success: 'success',
icon_status_warning: 'warning',
};
<script>
import ciIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
export default {
props: {
status: {
type: Object,
required: true,
},
},
components: {
ciIcon,
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${this.status.group}` : 'ci-status';
},
},
};
</script>
<template>
<a
:href="status.details_path"
:class="cssClass">
<ci-icon :status="status" />
{{status.text}}
</a>
</template>
<script>
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons';
import { statusIconEntityMap } from '../ci_status_icons';
 
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
props: {
status: {
Loading
Loading
@@ -15,7 +36,7 @@
},
 
cssClass() {
const status = statusCssClasses[this.status.icon];
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
Loading
Loading
import { statusClassToSvgMap } from '../pipeline_svg_icons';
export default {
name: 'PipelineStatusIcon',
props: {
pipelineStatus: { type: Object, required: true, default: () => ({}) },
},
computed: {
svg() {
return statusClassToSvgMap[this.pipelineStatus.icon];
},
statusClass() {
return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
},
},
template: `
<div :class="statusClass">
<a class="icon-link" :href="pipelineStatus.details_path">
<span v-html="svg" aria-hidden="true"></span>
</a>
</div>
`,
};
Loading
Loading
@@ -2,7 +2,7 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status';
import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
Loading
Loading
@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent,
ciBadge,
'time-ago': PipelinesTimeagoComponent,
},
 
Loading
Loading
@@ -196,11 +196,20 @@ export default {
 
return '';
},
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
},
},
 
template: `
<tr class="commit">
<status-scope :pipeline="pipeline"/>
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
 
<pipeline-url :pipeline="pipeline"></pipeline-url>
 
Loading
Loading
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
export const statusClassToSvgMap = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
export const statusClassToBorderlessSvgMap = {
icon_status_canceled: canceledBorderlessSvg,
icon_status_created: createdBorderlessSvg,
icon_status_failed: failedBorderlessSvg,
icon_status_manual: manualBorderlessSvg,
icon_status_pending: pendingBorderlessSvg,
icon_status_running: runningBorderlessSvg,
icon_status_skipped: skippedBorderlessSvg,
icon_status_success: successBorderlessSvg,
icon_status_warning: warningBorderlessSvg,
};
Loading
Loading
@@ -90,11 +90,6 @@
align-items: center;
padding: $gl-padding-top $gl-padding 0;
 
i,
svg {
margin-right: 8px;
}
svg {
position: relative;
top: 1px;
Loading
Loading
@@ -109,9 +104,10 @@
flex-wrap: wrap;
}
 
.ci-status-icon > .icon-link svg {
.icon-link > .ci-status-icon > svg {
width: 22px;
height: 22px;
margin-right: 8px;
}
}
 
Loading
Loading
---
title: Refactor all CI vue badges to use the same vue component
merge_request:
author:
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
 
const deploymentMockData = [
{
Loading
Loading
@@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => {
describe('svg', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent(deploymentMockData);
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success);
});
});
});
Loading
Loading
import Vue from 'vue';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data';
 
Loading
Loading
@@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => {
describe('components', () => {
it('should have components added', () => {
expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
expect(pipelineComponent.components.ciIcon).toBeDefined();
});
});
 
Loading
Loading
@@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent({ pipeline: mockData.pipeline });
 
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
});
});
 
Loading
Loading
import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
describe('CI Badge Link Component', () => {
let CIBadge;
const statuses = {
canceled: {
text: 'canceled',
label: 'canceled',
group: 'canceled',
icon: 'icon_status_canceled',
details_path: 'status/canceled',
},
created: {
text: 'created',
label: 'created',
group: 'created',
icon: 'icon_status_created',
details_path: 'status/created',
},
failed: {
text: 'failed',
label: 'failed',
group: 'failed',
icon: 'icon_status_failed',
details_path: 'status/failed',
},
manual: {
text: 'manual',
label: 'manual action',
group: 'manual',
icon: 'icon_status_manual',
details_path: 'status/manual',
},
pending: {
text: 'pending',
label: 'pending',
group: 'pending',
icon: 'icon_status_pending',
details_path: 'status/pending',
},
running: {
text: 'running',
label: 'running',
group: 'running',
icon: 'icon_status_running',
details_path: 'status/running',
},
skipped: {
text: 'skipped',
label: 'skipped',
group: 'skipped',
icon: 'icon_status_skipped',
details_path: 'status/skipped',
},
success_warining: {
text: 'passed',
label: 'passed',
group: 'success_with_warnings',
icon: 'icon_status_warning',
details_path: 'status/warning',
},
success: {
text: 'passed',
label: 'passed',
group: 'passed',
icon: 'icon_status_success',
details_path: 'status/passed',
},
};
it('should render each status badge', () => {
CIBadge = Vue.extend(ciBadge);
Object.keys(statuses).map((status) => {
const vm = new CIBadge({
propsData: {
status: statuses[status],
},
}).$mount();
expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`);
expect(vm.$el.querySelector('svg')).toBeDefined();
return vm;
});
});
});
Loading
Loading
@@ -25,6 +25,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_success',
group: 'success',
},
},
}).$mount();
Loading
Loading
@@ -37,6 +38,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_failed',
group: 'failed',
},
},
}).$mount();
Loading
Loading
@@ -49,6 +51,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_warning',
group: 'warning',
},
},
}).$mount();
Loading
Loading
@@ -61,6 +64,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_pending',
group: 'pending',
},
},
}).$mount();
Loading
Loading
@@ -73,6 +77,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_running',
group: 'running',
},
},
}).$mount();
Loading
Loading
@@ -85,6 +90,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_created',
group: 'created',
},
},
}).$mount();
Loading
Loading
@@ -97,6 +103,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_skipped',
group: 'skipped',
},
},
}).$mount();
Loading
Loading
@@ -109,6 +116,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_canceled',
group: 'canceled',
},
},
}).$mount();
Loading
Loading
@@ -121,6 +129,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_manual',
group: 'manual',
},
},
}).$mount();
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment