Commit c4a8f041 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '35570-add-to-the-environment-view-pod-state-legend' into 'master'

Add to the Environment view pod state legend

See merge request gitlab-org/gitlab!20208
parents 53faa9c8 c5e1b0f9
---
title: Added legend to deploy boards
merge_request: 20208
author:
type: added
......@@ -10,10 +10,10 @@
* [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
import _ from 'underscore';
import { n__, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { STATUS_MAP, CANARY_STATUS } from '../constants';
export default {
components: {
......@@ -22,7 +22,7 @@ export default {
GlLink,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
deployBoardData: {
......@@ -93,6 +93,17 @@ export default {
deployBoardSvg() {
return deployBoardSvg;
},
deployBoardActions() {
return this.deployBoardData.rollback_url || this.deployBoardData.abort_url;
},
statuses() {
// Canary is not a pod status but it needs to be in the legend.
// Hence adding it here.
return {
...STATUS_MAP,
CANARY_STATUS,
};
},
},
};
</script>
......@@ -107,56 +118,65 @@ 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-gl-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 gl-font-size-14 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>
<section v-if="deployBoardActions" class="deploy-board-actions">
<gl-link
v-if="deployBoardData.rollback_url"
:href="deployBoardData.rollback_url"
class="btn"
data-method="post"
rel="nofollow"
>{{ __('Rollback') }}</gl-link
>
<gl-link
v-if="deployBoardData.abort_url"
:href="deployBoardData.abort_url"
class="btn btn-red btn-inverted"
data-method="post"
rel="nofollow"
>{{ __('Abort') }}</gl-link
>
</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
export 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 const CANARY_STATUS = {
class: 'canary-icon',
text: __('Canary'),
stable: false,
};
......@@ -12,11 +12,14 @@
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab/issues/35570
*/
import tooltip from '~/vue_shared/directives/tooltip';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
export default {
components: {
GlLink,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
......@@ -52,32 +55,36 @@ 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;
},
},
};
</script>
<template>
<a
v-tooltip
<gl-link
v-gl-tooltip
:class="cssClass"
:data-title="tooltipText"
:title="tooltipText"
:href="computedLogPath"
class="deployment-instance d-flex justify-content-center align-items-center"
data-placement="top"
>
</a>
/>
</template>
.deployment-instance {
width: 15px;
height: 15px;
width: $gl-padding;
height: $gl-padding;
margin: 1px;
border: 1px solid;
border-radius: $border-radius-small;
......@@ -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,7 +61,7 @@
background-color: $white-light;
border-color: $gray-700;
&:hover {
&.link:hover {
background-color: $white-light;
border-color: $gray-900;
}
......@@ -78,4 +78,14 @@
z-index: 1;
}
}
&-canary-icon {
background-color: transparent;
border: 0;
&::after {
width: $gl-padding !important;
height: $gl-padding !important;
}
}
}
......@@ -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 {
......@@ -60,6 +60,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-line-height-14;
}
}
.deploy-board-icon {
......
......@@ -110,4 +110,35 @@ describe('Deploy Board', () => {
);
});
});
describe('has legend component', () => {
let statuses = [];
beforeEach(done => {
wrapper = createComponent({
isLoading: false,
isEmpty: false,
logsPath: environment.log_path,
hasLegacyAppLabel: true,
deployBoardData: deployBoardMockData,
});
({ statuses } = wrapper.vm);
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(Object.keys(statuses).length);
});
Object.keys(statuses).forEach(item => {
it(`with ${item} text next to deployment instance icon`, () => {
expect(wrapper.find(`.deployment-instance-${item}`)).toBeDefined();
expect(wrapper.find(`.deployment-instance-${item} + .legend-text`).text()).toBe(
statuses[item].text,
);
});
});
});
});
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('');
});
});
});
......@@ -2960,6 +2960,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
msgid "Canary"
msgstr ""
msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
msgstr ""
......@@ -16963,6 +16966,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