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 @@ ...@@ -10,10 +10,10 @@
* [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png) * [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/ */
import _ from 'underscore'; import _ from 'underscore';
import { n__, s__, sprintf } from '~/locale'; import { GlLoadingIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg'; 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 { export default {
components: { components: {
...@@ -22,7 +22,7 @@ export default { ...@@ -22,7 +22,7 @@ export default {
GlLink, GlLink,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
deployBoardData: { deployBoardData: {
...@@ -93,6 +93,17 @@ export default { ...@@ -93,6 +93,17 @@ export default {
deployBoardSvg() { deployBoardSvg() {
return 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> </script>
...@@ -107,56 +118,65 @@ export default { ...@@ -107,56 +118,65 @@ export default {
</gl-link> </gl-link>
</div> </div>
<div v-if="canRenderDeployBoard" class="deploy-board-information"> <div v-if="canRenderDeployBoard" class="deploy-board-information p-3">
<section class="deploy-board-status"> <div class="deploy-board-information">
<span v-tooltip :title="instanceIsCompletedText"> <section class="deploy-board-status">
<span ref="percentage" class="text-center text-plain gl-font-size-large" <span v-gl-tooltip :title="instanceIsCompletedText">
>{{ deployBoardData.completion }}%</span <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> <span class="text text-center text-secondary">{{ __('Complete') }}</span>
</section> </span>
</section>
<section class="deploy-board-instances"> <section class="deploy-board-instances">
<span class="deploy-board-instances-text text-secondary"> <span class="deploy-board-instances-text gl-font-size-14 text-secondary">
{{ instanceTitle }} ({{ instanceCount }}) {{ instanceTitle }} ({{ instanceCount }})
</span> </span>
<div class="deploy-board-instances-container d-flex flex-wrap flex-row"> <div class="deploy-board-instances-container d-flex flex-wrap flex-row">
<template v-for="(instance, i) in deployBoardData.instances"> <template v-for="(instance, i) in deployBoardData.instances">
<instance-component <instance-component
:key="i" :key="i"
:status="instance.status" :status="instance.status"
:tooltip-text="instance.tooltip" :tooltip-text="instance.tooltip"
:pod-name="instance.pod_name" :pod-name="instance.pod_name"
:logs-path="logsPath" :logs-path="logsPath"
:stable="instance.stable" :stable="instance.stable"
/> />
</template> </template>
</div> </div>
</section> <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 <section v-if="deployBoardActions" class="deploy-board-actions">
v-if="deployBoardData.rollback_url || deployBoardData.abort_url" <gl-link
class="deploy-board-actions" v-if="deployBoardData.rollback_url"
> :href="deployBoardData.rollback_url"
<a class="btn"
v-if="deployBoardData.rollback_url" data-method="post"
:href="deployBoardData.rollback_url" rel="nofollow"
class="btn" >{{ __('Rollback') }}</gl-link
data-method="post" >
rel="nofollow" <gl-link
>{{ __('Rollback') }}</a v-if="deployBoardData.abort_url"
> :href="deployBoardData.abort_url"
<a class="btn btn-red btn-inverted"
v-if="deployBoardData.abort_url" data-method="post"
:href="deployBoardData.abort_url" rel="nofollow"
class="btn btn-red btn-inverted" >{{ __('Abort') }}</gl-link
data-method="post" >
rel="nofollow" </section>
>{{ __('Abort') }}</a </div>
>
</section>
</div> </div>
<div v-if="canRenderEmptyState" class="deploy-board-empty"> <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 @@ ...@@ -12,11 +12,14 @@
* this information in the tooltip and the colors. * this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab/issues/35570 * 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 { export default {
components: {
GlLink,
},
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
...@@ -52,32 +55,36 @@ export default { ...@@ -52,32 +55,36 @@ export default {
logsPath: { logsPath: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
computed: { computed: {
isLink() {
return Boolean(this.logsPath || this.podName);
},
cssClass() { cssClass() {
return { return {
[`deployment-instance-${this.status}`]: true, [`deployment-instance-${this.status}`]: true,
'deployment-instance-canary': !this.stable, 'deployment-instance-canary': !this.stable,
link: this.isLink,
}; };
}, },
computedLogPath() { computedLogPath() {
return `${this.logsPath}?pod_name=${this.podName}`; return this.isLink ? `${this.logsPath}?pod_name=${this.podName}` : null;
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <gl-link
v-tooltip v-gl-tooltip
:class="cssClass" :class="cssClass"
:data-title="tooltipText" :title="tooltipText"
:href="computedLogPath" :href="computedLogPath"
class="deployment-instance d-flex justify-content-center align-items-center" class="deployment-instance d-flex justify-content-center align-items-center"
data-placement="top" />
>
</a>
</template> </template>
.deployment-instance { .deployment-instance {
width: 15px; width: $gl-padding;
height: 15px; height: $gl-padding;
margin: 1px; margin: 1px;
border: 1px solid; border: 1px solid;
border-radius: $border-radius-small; border-radius: $border-radius-small;
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
background-color: $green-600; background-color: $green-600;
border-color: $green-800; border-color: $green-800;
&:hover { &.link:hover {
background-color: $green-800; background-color: $green-800;
border-color: $green-950; border-color: $green-950;
} }
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
background-color: $green-300; background-color: $green-300;
border-color: $green-600; border-color: $green-600;
&:hover { &.link:hover {
background-color: $green-500; background-color: $green-500;
border-color: $green-800; border-color: $green-800;
} }
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
bottom: -2px; bottom: -2px;
} }
&:hover { &.link:hover {
background-color: $red-800; background-color: $red-800;
border-color: $red-950; border-color: $red-950;
} }
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
background-color: $gray-300; background-color: $gray-300;
border-color: $gray-700; border-color: $gray-700;
&:hover { &.link:hover {
background-color: $gray-500; background-color: $gray-500;
border-color: $gray-900; border-color: $gray-900;
} }
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
background-color: $white-light; background-color: $white-light;
border-color: $gray-700; border-color: $gray-700;
&:hover { &.link:hover {
background-color: $white-light; background-color: $white-light;
border-color: $gray-900; border-color: $gray-900;
} }
...@@ -78,4 +78,14 @@ ...@@ -78,4 +78,14 @@
z-index: 1; z-index: 1;
} }
} }
&-canary-icon {
background-color: transparent;
border: 0;
&::after {
width: $gl-padding !important;
height: $gl-padding !important;
}
}
} }
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
padding: 10px; padding: 10px;
} }
> .deploy-board-information { .deploy-board-information {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
width: 70px; width: 70px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
margin: 20px 0 20px 5px; margin: 20px 0 0 5px;
} }
.deploy-board-instances { .deploy-board-instances {
...@@ -60,6 +60,13 @@ ...@@ -60,6 +60,13 @@
line-height: 40px; 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 { .deploy-board-icon {
......
...@@ -110,4 +110,35 @@ describe('Deploy Board', () => { ...@@ -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 Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import DeployBoardInstance from 'ee/vue_shared/components/deployment_instance.vue'; import DeployBoardInstance from 'ee/vue_shared/components/deployment_instance.vue';
import { folder } from '../../environments/mock_data'; import { folder } from '../../environments/mock_data';
describe('Deploy Board Instance', () => { describe('Deploy Board Instance', () => {
let DeployBoardInstanceComponent; let wrapper;
const DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance);
beforeEach(() => { const createComponent = (props = {}) =>
DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance); shallowMount(DeployBoardInstanceComponent, {
});
it('should render a div with the correct css status and tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: { propsData: {
status: 'ready', status: 'succeeded',
tooltipText: 'This is a pod', ...props,
logsPath: folder.log_path,
}, },
}).$mount(); sync: false,
});
expect(component.$el.classList.contains('deployment-instance-ready')).toBe(true); describe('as a non-canary deployment', () => {
expect(component.$el.getAttribute('data-title')).toEqual('This is a pod'); afterEach(() => {
}); wrapper.destroy();
});
it('should render a div without tooltip data', () => { it('should render a div with the correct css status and tooltip data', () => {
const component = new DeployBoardInstanceComponent({ wrapper = createComponent({
propsData: { 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', 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, logsPath: folder.log_path,
}, podName: 'tanuki-1',
}).$mount(); });
expect(component.$el.classList.contains('deployment-instance-deploying')).toBe(true); expect(wrapper.vm.computedLogPath).toEqual(
expect(component.$el.getAttribute('data-title')).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', () => { describe('as a canary deployment', () => {
const component = new DeployBoardInstanceComponent({ afterEach(() => {
propsData: { wrapper.destroy();
status: 'deploying', });
it('should render a div with canary class when stable prop is provided as false', done => {
wrapper = createComponent({
stable: false, 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', () => { describe('as a legend item', () => {
const component = new DeployBoardInstanceComponent({ afterEach(() => {
propsData: { wrapper.destroy();
status: 'deploying', });
it('should not be a link without a logsPath prop', done => {
wrapper = createComponent({
stable: false, stable: false,
logsPath: folder.log_path, logsPath: '',
podName: 'tanuki-1', });
},
}).$mount(); 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( expect(wrapper.attributes('data-original-title')).toEqual('');
'/root/review-app/environments/12/logs?pod_name=tanuki-1', });
);
}); });
}); });
...@@ -2960,6 +2960,9 @@ msgstr "" ...@@ -2960,6 +2960,9 @@ msgstr ""
msgid "Can't scan the code?" msgid "Can't scan the code?"
msgstr "" 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." 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 "" msgstr ""
...@@ -16963,6 +16966,9 @@ msgstr "" ...@@ -16963,6 +16966,9 @@ msgstr ""
msgid "Subtracts" msgid "Subtracts"
msgstr "" msgstr ""
msgid "Succeeded"
msgstr ""
msgid "Successfully activated" msgid "Successfully activated"
msgstr "" 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