Commit a4836a1a authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Deploy board legend

Added legend for deploy instances in
deploy boards explaining the pod status
parent 61370941
---
title: Added legend to deploy boards
merge_request: 20208
author:
type: added
......@@ -10,10 +10,11 @@
* [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
import _ from 'underscore';
import { n__, s__, sprintf } from '~/locale';
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import { __, n__, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import STATUS_MAP from '../constants';
export default {
components: {
......@@ -93,6 +94,16 @@ export default {
deployBoardSvg() {
return deployBoardSvg;
},
statuses() {
return {
...STATUS_MAP,
transparent: {
class: 'transparent',
text: __('Canary'),
stable: false,
},
};
},
},
};
</script>
......@@ -107,56 +118,68 @@ export default {
</gl-link>
</div>
<div v-if="canRenderDeployBoard" class="deploy-board-information">
<section class="deploy-board-status">
<span v-tooltip :title="instanceIsCompletedText">
<span ref="percentage" class="text-center text-plain gl-font-size-large"
>{{ deployBoardData.completion }}%</span
>
<span class="text text-center text-secondary">{{ __('Complete') }}</span>
</span>
</section>
<div v-if="canRenderDeployBoard" class="deploy-board-information p-3">
<div class="deploy-board-information">
<section class="deploy-board-status">
<span v-tooltip :title="instanceIsCompletedText">
<span ref="percentage" class="text-center text-plain gl-font-size-large"
>{{ deployBoardData.completion }}%</span
>
<span class="text text-center text-secondary">{{ __('Complete') }}</span>
</span>
</section>
<section class="deploy-board-instances">
<span class="deploy-board-instances-text text-secondary">
{{ instanceTitle }} ({{ instanceCount }})
</span>
<section class="deploy-board-instances">
<span class="deploy-board-instances-text text-secondary">
{{ instanceTitle }} ({{ instanceCount }})
</span>
<div class="deploy-board-instances-container d-flex flex-wrap flex-row">
<template v-for="(instance, i) in deployBoardData.instances">
<instance-component
:key="i"
:status="instance.status"
:tooltip-text="instance.tooltip"
:pod-name="instance.pod_name"
:logs-path="logsPath"
:stable="instance.stable"
/>
</template>
</div>
</section>
<div class="deploy-board-instances-container d-flex flex-wrap flex-row">
<template v-for="(instance, i) in deployBoardData.instances">
<instance-component
:key="i"
:status="instance.status"
:tooltip-text="instance.tooltip"
:pod-name="instance.pod_name"
:logs-path="logsPath"
:stable="instance.stable"
/>
</template>
</div>
<div class="deploy-board-legend d-flex mt-3">
<div
v-for="status in statuses"
:key="status.text"
class="d-flex justify-content-center align-items-center mr-3"
>
<instance-component :status="status.class" :stable="status.stable" />
<span class="legend-text ml-2">{{ status.text }}</span>
</div>
</div>
</section>
<section
v-if="deployBoardData.rollback_url || deployBoardData.abort_url"
class="deploy-board-actions"
>
<a
v-if="deployBoardData.rollback_url"
:href="deployBoardData.rollback_url"
class="btn"
data-method="post"
rel="nofollow"
>{{ __('Rollback') }}</a
>
<a
v-if="deployBoardData.abort_url"
:href="deployBoardData.abort_url"
class="btn btn-red btn-inverted"
data-method="post"
rel="nofollow"
>{{ __('Abort') }}</a
<section
v-if="deployBoardData.rollback_url || deployBoardData.abort_url"
class="deploy-board-actions"
>
</section>
<a
v-if="deployBoardData.rollback_url"
:href="deployBoardData.rollback_url"
class="btn"
data-method="post"
rel="nofollow"
>{{ __('Rollback') }}</a
>
<a
v-if="deployBoardData.abort_url"
:href="deployBoardData.abort_url"
class="btn btn-red btn-inverted"
data-method="post"
rel="nofollow"
>{{ __('Abort') }}</a
>
</section>
</div>
</div>
<div v-if="canRenderEmptyState" class="deploy-board-empty">
......
import { __ } from '~/locale';
// These statuses are based on how the backend defines pod phases here
// lib/gitlab/kubernetes/pod.rb
const STATUS_MAP = {
succeeded: {
class: 'succeeded',
text: __('Succeeded'),
stable: true,
},
running: {
class: 'running',
text: __('Running'),
stable: true,
},
failed: {
class: 'failed',
text: __('Failed'),
stable: true,
},
pending: {
class: 'pending',
text: __('Pending'),
stable: true,
},
unknown: {
class: 'unknown',
text: __('Unknown'),
stable: true,
},
};
export default STATUS_MAP;
......@@ -52,20 +52,26 @@ export default {
logsPath: {
type: String,
required: true,
required: false,
default: '',
},
},
computed: {
isLink() {
return Boolean(this.logsPath || this.podName);
},
cssClass() {
return {
[`deployment-instance-${this.status}`]: true,
'deployment-instance-canary': !this.stable,
link: this.isLink,
};
},
computedLogPath() {
return `${this.logsPath}?pod_name=${this.podName}`;
return this.isLink ? `${this.logsPath}?pod_name=${this.podName}` : null;
},
},
};
......@@ -74,7 +80,7 @@ export default {
<a
v-tooltip
:class="cssClass"
:data-title="tooltipText"
:title="tooltipText"
:href="computedLogPath"
class="deployment-instance d-flex justify-content-center align-items-center"
data-placement="top"
......
......@@ -10,7 +10,7 @@
background-color: $green-600;
border-color: $green-800;
&:hover {
&.link:hover {
background-color: $green-800;
border-color: $green-950;
}
......@@ -20,7 +20,7 @@
background-color: $green-300;
border-color: $green-600;
&:hover {
&.link:hover {
background-color: $green-500;
border-color: $green-800;
}
......@@ -41,7 +41,7 @@
bottom: -2px;
}
&:hover {
&.link:hover {
background-color: $red-800;
border-color: $red-950;
}
......@@ -51,7 +51,7 @@
background-color: $gray-300;
border-color: $gray-700;
&:hover {
&.link:hover {
background-color: $gray-500;
border-color: $gray-900;
}
......@@ -61,12 +61,17 @@
background-color: $white-light;
border-color: $gray-700;
&:hover {
&.link:hover {
background-color: $white-light;
border-color: $gray-900;
}
}
&-transparent {
background-color: transparent;
border: none;
}
&.deployment-instance-canary {
&::after {
width: 7px;
......
......@@ -11,7 +11,7 @@
padding: 10px;
}
> .deploy-board-information {
.deploy-board-information {
display: flex;
justify-content: space-between;
......@@ -21,7 +21,7 @@
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 20px 5px;
margin: 20px 0 0 5px;
}
.deploy-board-instances {
......@@ -30,6 +30,17 @@
width: 100%;
}
.deploy-board-instances-text {
font-size: $gl-font-size;
color: $gl-text-color-secondary;
}
/*
.deploy-board-instances-container {
margin-top: -8px;
}
*/
.deploy-board-actions {
order: 3;
align-self: center;
......@@ -60,6 +71,13 @@
line-height: 40px;
}
}
.deploy-board-legend .legend-text {
color: $gl-text-color;
font-size: $gl-font-size-small;
font-weight: $gl-font-weight-bold;
line-height: $gl-font-size-small;
}
}
.deploy-board-icon {
......
......@@ -110,4 +110,24 @@ describe('Deploy Board', () => {
);
});
});
describe('has legend component', () => {
beforeEach(done => {
wrapper = createComponent({
isLoading: false,
isEmpty: false,
logsPath: environment.log_path,
hasLegacyAppLabel: true,
deployBoardData: deployBoardMockData,
});
wrapper.vm.$nextTick(done);
});
it('with all the possible statuses', () => {
const deployBoardLegend = wrapper.find('.deploy-board-legend');
expect(deployBoardLegend).toBeDefined();
expect(deployBoardLegend.findAll('a').length).toBe(6);
});
});
});
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import DeployBoardInstance from 'ee/vue_shared/components/deployment_instance.vue';
import { folder } from '../../environments/mock_data';
describe('Deploy Board Instance', () => {
let DeployBoardInstanceComponent;
let wrapper;
const DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance);
beforeEach(() => {
DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance);
});
it('should render a div with the correct css status and tooltip data', () => {
const component = new DeployBoardInstanceComponent({
const createComponent = (props = {}) =>
shallowMount(DeployBoardInstanceComponent, {
propsData: {
status: 'ready',
tooltipText: 'This is a pod',
logsPath: folder.log_path,
status: 'succeeded',
...props,
},
}).$mount();
sync: false,
});
expect(component.$el.classList.contains('deployment-instance-ready')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('This is a pod');
});
describe('as a non-canary deployment', () => {
afterEach(() => {
wrapper.destroy();
});
it('should render a div without tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
it('should render a div with the correct css status and tooltip data', () => {
wrapper = createComponent({
logsPath: folder.log_path,
tooltipText: 'This is a pod',
});
expect(wrapper.classes('deployment-instance-succeeded')).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual('This is a pod');
});
it('should render a div without tooltip data', done => {
wrapper = createComponent({
status: 'deploying',
tooltipText: '',
});
wrapper.vm.$nextTick(() => {
expect(wrapper.classes('deployment-instance-deploying')).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual('');
done();
});
});
it('should have a log path computed with a pod name as a parameter', () => {
wrapper = createComponent({
logsPath: folder.log_path,
},
}).$mount();
podName: 'tanuki-1',
});
expect(component.$el.classList.contains('deployment-instance-deploying')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('');
expect(wrapper.vm.computedLogPath).toEqual(
'/root/review-app/environments/12/logs?pod_name=tanuki-1',
);
});
});
it('should render a div with canary class when stable prop is provided as false', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
describe('as a canary deployment', () => {
afterEach(() => {
wrapper.destroy();
});
it('should render a div with canary class when stable prop is provided as false', done => {
wrapper = createComponent({
stable: false,
logsPath: folder.log_path,
},
}).$mount();
});
expect(component.$el.classList.contains('deployment-instance-canary')).toBe(true);
wrapper.vm.$nextTick(() => {
expect(wrapper.classes('deployment-instance-canary')).toBe(true);
done();
});
});
});
it('should have a log path computed with a pod name as a parameter', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
describe('as a legend item', () => {
afterEach(() => {
wrapper.destroy();
});
it('should not be a link without a logsPath prop', done => {
wrapper = createComponent({
stable: false,
logsPath: folder.log_path,
podName: 'tanuki-1',
},
}).$mount();
logsPath: '',
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.computedLogPath).toBeNull();
expect(wrapper.vm.isLink).toBeFalsy();
done();
});
});
it('should render a link without href if path is not passed', () => {
wrapper = createComponent();
expect(wrapper.attributes('href')).toBeUndefined();
});
it('should not have a tooltip', () => {
wrapper = createComponent();
expect(component.computedLogPath).toEqual(
'/root/review-app/environments/12/logs?pod_name=tanuki-1',
);
expect(wrapper.attributes('data-original-title')).toEqual('');
});
});
});
......@@ -16963,6 +16963,9 @@ msgstr ""
msgid "Subtracts"
msgstr ""
msgid "Succeeded"
msgstr ""
msgid "Successfully activated"
msgstr ""
......
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