Commit 0e1bd185 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'canary-ingress-ui' into 'master'

Canary Ingress UI RUN-AS-IF-FOSS

See merge request gitlab-org/gitlab!49516
parents 71ef455d 855a72e0
...@@ -143,13 +143,7 @@ export default { ...@@ -143,13 +143,7 @@ export default {
<confirm-rollback-modal :environment="environmentInRollbackModal" /> <confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="gl-w-full"> <div class="gl-w-full">
<div <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-display-md-none!">
class="
gl-display-flex
gl-flex-direction-column
gl-mt-3
gl-display-md-none!"
>
<gl-button <gl-button
v-if="state.reviewAppDetails.can_setup_review_app" v-if="state.reviewAppDetails.can_setup_review_app"
v-gl-modal="$options.modal.id" v-gl-modal="$options.modal.id"
...@@ -158,18 +152,16 @@ export default { ...@@ -158,18 +152,16 @@ export default {
category="secondary" category="secondary"
type="button" type="button"
class="gl-mb-3 gl-flex-fill-1" class="gl-mb-3 gl-flex-fill-1"
>{{ $options.i18n.reviewAppButtonLabel }}</gl-button
> >
{{ $options.i18n.reviewAppButtonLabel }}
</gl-button>
<gl-button <gl-button
v-if="canCreateEnvironment" v-if="canCreateEnvironment"
:href="newEnvironmentPath" :href="newEnvironmentPath"
data-testid="new-environment" data-testid="new-environment"
category="primary" category="primary"
variant="success" variant="success"
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
> >
{{ $options.i18n.newEnvironmentButtonLabel }}
</gl-button>
</div> </div>
<gl-tabs content-class="gl-display-none"> <gl-tabs content-class="gl-display-none">
<gl-tab <gl-tab
...@@ -185,14 +177,7 @@ export default { ...@@ -185,14 +177,7 @@ export default {
</gl-tab> </gl-tab>
<template #tabs-end> <template #tabs-end>
<div <div
class=" class="gl-display-none gl-display-md-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
gl-display-none
gl-display-md-flex
gl-lg-align-items-center
gl-lg-flex-direction-row
gl-lg-flex-fill-1
gl-lg-justify-content-end
gl-lg-mt-0"
> >
<gl-button <gl-button
v-if="state.reviewAppDetails.can_setup_review_app" v-if="state.reviewAppDetails.can_setup_review_app"
...@@ -202,18 +187,16 @@ export default { ...@@ -202,18 +187,16 @@ export default {
category="secondary" category="secondary"
type="button" type="button"
class="gl-mb-3 gl-lg-mr-3 gl-lg-mb-0" class="gl-mb-3 gl-lg-mr-3 gl-lg-mb-0"
>{{ $options.i18n.reviewAppButtonLabel }}</gl-button
> >
{{ $options.i18n.reviewAppButtonLabel }}
</gl-button>
<gl-button <gl-button
v-if="canCreateEnvironment" v-if="canCreateEnvironment"
:href="newEnvironmentPath" :href="newEnvironmentPath"
data-testid="new-environment" data-testid="new-environment"
category="primary" category="primary"
variant="success" variant="success"
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
> >
{{ $options.i18n.newEnvironmentButtonLabel }}
</gl-button>
</div> </div>
</template> </template>
</gl-tabs> </gl-tabs>
......
...@@ -15,6 +15,7 @@ export default { ...@@ -15,6 +15,7 @@ export default {
CanaryDeploymentCallout: () => CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'), import('ee_component/environments/components/canary_deployment_callout.vue'),
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'),
}, },
props: { props: {
environments: { environments: {
...@@ -58,6 +59,12 @@ export default { ...@@ -58,6 +59,12 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
canaryWeight: 0,
environmentToChange: null,
};
},
computed: { computed: {
sortedEnvironments() { sortedEnvironments() {
return this.sortEnvironments(this.environments).map(env => return this.sortEnvironments(this.environments).map(env =>
...@@ -144,11 +151,16 @@ export default { ...@@ -144,11 +151,16 @@ export default {
sortBy(env => (env.isFolder ? -1 : 1)), sortBy(env => (env.isFolder ? -1 : 1)),
)(environments); )(environments);
}, },
changeCanaryWeight(model, weight) {
this.environmentToChange = model;
this.canaryWeight = weight;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-table" role="grid"> <div class="ci-table" role="grid">
<canary-update-modal :environment="environmentToChange" :weight="canaryWeight" />
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section" :class="tableData.name.spacing" role="columnheader"> <div class="table-section" :class="tableData.name.spacing" role="columnheader">
{{ tableData.name.title }} {{ tableData.name.title }}
...@@ -179,6 +191,7 @@ export default { ...@@ -179,6 +191,7 @@ export default {
:model="model" :model="model"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:table-data="tableData" :table-data="tableData"
data-qa-selector="environment_item"
/> />
<div <div
...@@ -193,6 +206,7 @@ export default { ...@@ -193,6 +206,7 @@ export default {
:is-loading="model.isLoadingDeployBoard" :is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard" :is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path" :logs-path="model.logs_path"
@changeCanaryWeight="changeCanaryWeight(model, $event)"
/> />
</div> </div>
</div> </div>
...@@ -215,6 +229,7 @@ export default { ...@@ -215,6 +229,7 @@ export default {
:model="children" :model="children"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:table-data="tableData" :table-data="tableData"
data-qa-selector="environment_item"
/> />
<div :key="`sub-div-${i}`"> <div :key="`sub-div-${i}`">
......
...@@ -103,21 +103,22 @@ Here's an example setup flow from scratch: ...@@ -103,21 +103,22 @@ Here's an example setup flow from scratch:
#### How to check the current traffic weight on a Canary Ingress #### How to check the current traffic weight on a Canary Ingress
1. Visit [Deploy Board](../../user/project/deploy_boards.md). 1. Visit the [Deploy Board](../../user/project/deploy_boards.md).
1. Open your browser's inspection tool and examine a response from the `environments.json` endpoint. 1. View the current weights on the right.
You can find the current weight under `rollout_status`.
![Rollout Status Canary Ingress](img/rollout_status_canary_ingress.png) ![Rollout Status Canary Ingress](img/canary_weight.png)
Note that we have [a plan](https://gitlab.com/gitlab-org/gitlab/-/issues/218139)
to visualize this information in a [Deploy Board](../../user/project/deploy_boards.md)
without needing a browser's inspection tool.
#### How to change the traffic weight on a Canary Ingress #### How to change the traffic weight on a Canary Ingress
You can change the traffic weight by using [GraphiQL](../../api/graphql/getting_started.md#graphiql) You can change the traffic weight within your environment's Deploy Board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
or by sending requests to the [GraphQL API](../../api/graphql/getting_started.md#command-line). or by sending requests to the [GraphQL API](../../api/graphql/getting_started.md#command-line).
To use your [Deploy Board](../../user/project/deploy_boards.md):
1. Navigate to **Operations > Environments** for your project.
1. Set the new weight with the dropdown on the right side.
1. Confirm your selection.
Here's an example using [GraphiQL](../../api/graphql/getting_started.md#graphiql): Here's an example using [GraphiQL](../../api/graphql/getting_started.md#graphiql):
1. Visit [GraphiQL Explorer](https://gitlab.com/-/graphql-explorer). 1. Visit [GraphiQL Explorer](https://gitlab.com/-/graphql-explorer).
...@@ -136,6 +137,3 @@ Here's an example using [GraphiQL](../../api/graphql/getting_started.md#graphiql ...@@ -136,6 +137,3 @@ Here's an example using [GraphiQL](../../api/graphql/getting_started.md#graphiql
1. If the request succeeds, the `errors` response contains an empty array. GitLab sends a `PATCH` 1. If the request succeeds, the `errors` response contains an empty array. GitLab sends a `PATCH`
request to your Kubernetes cluster for updating the weight parameter on a Canary Ingress. request to your Kubernetes cluster for updating the weight parameter on a Canary Ingress.
Note that there's [a plan](https://gitlab.com/gitlab-org/gitlab/-/issues/218139)
to control the weight from a [Deploy Board](../../user/project/deploy_boards.md).
<script>
import { uniqueId } from 'lodash';
import { GlDropdown, GlDropdownItem, GlModalDirective as GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import { CANARY_UPDATE_MODAL } from '../constants';
export default {
components: {
GlDropdown,
GlDropdownItem,
},
directives: {
GlModal,
},
props: {
canaryIngress: {
required: true,
type: Object,
},
},
ingressOptions: Array(100 / 5 + 1)
.fill(0)
.map((_, i) => i * 5),
translations: {
stableLabel: s__('CanaryIngress|Stable'),
canaryLabel: s__('CanaryIngress|Canary'),
},
CANARY_UPDATE_MODAL,
css: {
label: [
'gl-font-base',
'gl-font-weight-normal',
'gl-line-height-normal',
'gl-inset-border-1-gray-200',
'gl-py-3',
'gl-px-4',
'gl-mb-0',
],
},
computed: {
stableWeightId() {
return uniqueId('stable-weight-');
},
canaryWeightId() {
return uniqueId('canary-weight-');
},
stableWeight() {
return (100 - this.canaryIngress.canary_weight).toString();
},
canaryWeight() {
return this.canaryIngress.canary_weight.toString();
},
},
methods: {
changeCanary(weight) {
this.$emit('change', weight);
},
changeStable(weight) {
this.$emit('change', 100 - weight);
},
},
};
</script>
<template>
<section class="gl-display-flex gl-bg-white gl-m-3">
<div class="gl-display-flex gl-flex-direction-column">
<label :for="stableWeightId" :class="$options.css.label" class="gl-rounded-top-left-base">
{{ $options.translations.stableLabel }}
</label>
<gl-dropdown
:id="stableWeightId"
:text="stableWeight"
data-testid="stable-weight"
class="gl-w-full"
toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
>
<gl-dropdown-item
v-for="option in $options.ingressOptions"
:key="option"
v-gl-modal="$options.CANARY_UPDATE_MODAL"
@click="changeStable(option)"
>{{ option }}</gl-dropdown-item
>
</gl-dropdown>
</div>
<div class="gl-display-flex gl-display-flex gl-flex-direction-column">
<label :for="canaryWeightId" :class="$options.css.label" class="gl-rounded-top-right-base">{{
$options.translations.canaryLabel
}}</label>
<gl-dropdown
:id="canaryWeightId"
:text="canaryWeight"
data-testid="canary-weight"
toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-left-none! gl-border-l-none!"
>
<gl-dropdown-item
v-for="option in $options.ingressOptions"
:key="option"
v-gl-modal="$options.CANARY_UPDATE_MODAL"
@click="changeCanary(option)"
>{{ option }}</gl-dropdown-item
>
</gl-dropdown>
</div>
</section>
</template>
<script>
import { GlAlert, GlModal, GlSprintf } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import updateCanaryIngress from '../graphql/mutations/update_canary_ingress.mutation.graphql';
import { CANARY_UPDATE_MODAL } from '../constants';
export default {
components: {
GlAlert,
GlModal,
GlSprintf,
},
props: {
environment: {
type: Object,
required: false,
default: () => ({}),
},
weight: {
type: Number,
required: false,
default: 0,
},
visible: {
type: Boolean,
required: false,
default: false,
},
},
translations: {
title: s__('CanaryIngress|Change the ratio of canary deployments?'),
ratioChange: s__(
'CanaryIngress|You are changing the ratio of the canary rollout for %{environment} compared to the stable deployment to:',
),
stableWeight: s__('CanaryIngress|%{boldStart}Stable:%{boldEnd} %{stable}'),
canaryWeight: s__('CanaryIngress|%{boldStart}Canary:%{boldEnd} %{canary}'),
deploymentWarning: s__(
'CanaryIngress|Doing so will set a deployment change in progress. This temporarily blocks any further configuration until the deployment is finished.',
),
},
modal: {
modalId: CANARY_UPDATE_MODAL,
actionPrimary: {
text: s__('CanaryIngress|Change ratio'),
attributes: [{ variant: 'info' }],
},
actionCancel: { text: __('Cancel') },
static: true,
},
data() {
return { error: '', dismissed: true };
},
computed: {
stableWeight() {
return (100 - this.weight).toString();
},
canaryWeight() {
return this.weight.toString();
},
hasError() {
return Boolean(this.error);
},
environmentName() {
return this.environment?.name ?? '';
},
},
methods: {
submitCanaryChange() {
return this.$apollo
.mutate({
mutation: updateCanaryIngress,
variables: {
input: {
id: this.environment.global_id,
weight: this.weight,
},
},
})
.then(({ data: { environmentsCanaryIngressUpdate: { errors: [error] } } }) => {
this.error = error;
})
.catch(() => {
this.error = __('Something went wrong. Please try again later');
});
},
dismiss() {
this.error = '';
},
},
};
</script>
<template>
<div>
<gl-alert v-if="hasError" variant="danger" @dismiss="dismiss">{{ error }}</gl-alert>
<gl-modal v-bind="$options.modal" :visible="visible" @primary="submitCanaryChange">
<template #modal-title>{{ $options.translations.title }}</template>
<template #default>
<p>
<gl-sprintf :message="$options.translations.ratioChange">
<template #environment>{{ environmentName }}</template>
</gl-sprintf>
</p>
<ul class="gl-list-style-none gl-p-0">
<li>
<gl-sprintf :message="$options.translations.stableWeight">
<template #bold="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span>
</template>
<template #stable>{{ stableWeight }}</template>
</gl-sprintf>
</li>
<li>
<gl-sprintf :message="$options.translations.canaryWeight">
<template #bold="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span>
</template>
<template #canary>{{ canaryWeight }}</template>
</gl-sprintf>
</li>
</ul>
<p>{{ $options.translations.deploymentWarning }}</p>
</template>
</gl-modal>
</div>
</template>
...@@ -20,11 +20,14 @@ import { ...@@ -20,11 +20,14 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg'; import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATUS_MAP, CANARY_STATUS } from '../constants'; import { STATUS_MAP, CANARY_STATUS } from '../constants';
import CanaryIngress from './canary_ingress.vue';
export default { export default {
components: { components: {
instanceComponent: () => import('ee_component/vue_shared/components/deployment_instance.vue'), instanceComponent: () => import('ee_component/vue_shared/components/deployment_instance.vue'),
CanaryIngress,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
GlLink, GlLink,
...@@ -34,6 +37,7 @@ export default { ...@@ -34,6 +37,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
SafeHtml, SafeHtml,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
deployBoardData: { deployBoardData: {
type: Object, type: Object,
...@@ -65,6 +69,11 @@ export default { ...@@ -65,6 +69,11 @@ export default {
canRenderEmptyState() { canRenderEmptyState() {
return this.isEmpty; return this.isEmpty;
}, },
canRenderCanaryWeight() {
return (
this.glFeatures.canaryIngressWeightControl && !isEmpty(this.deployBoardData.canary_ingress)
);
},
instanceCount() { instanceCount() {
const { instances } = this.deployBoardData; const { instances } = this.deployBoardData;
...@@ -99,17 +108,22 @@ export default { ...@@ -99,17 +108,22 @@ export default {
}; };
}, },
}, },
methods: {
changeCanaryWeight(weight) {
this.$emit('changeCanaryWeight', weight);
},
},
}; };
</script> </script>
<template> <template>
<div class="js-deploy-board deploy-board"> <div class="js-deploy-board deploy-board">
<gl-loading-icon v-if="isLoading" class="loading-icon" /> <gl-loading-icon v-if="isLoading" class="loading-icon" />
<template v-else> <template v-else>
<div v-if="canRenderDeployBoard" class="deploy-board-information p-3"> <div v-if="canRenderDeployBoard" class="deploy-board-information gl-p-5">
<div class="deploy-board-information"> <div class="deploy-board-information gl-w-full">
<section class="deploy-board-status"> <section class="deploy-board-status">
<span v-gl-tooltip :title="instanceIsCompletedText"> <span v-gl-tooltip :title="instanceIsCompletedText">
<span ref="percentage" class="text-center text-plain gl-font-lg" <span ref="percentage" class="gl-text-center text-plain gl-font-lg"
>{{ deployBoardData.completion }}%</span >{{ deployBoardData.completion }}%</span
> >
<span class="text text-center text-secondary">{{ __('Complete') }}</span> <span class="text text-center text-secondary">{{ __('Complete') }}</span>
...@@ -152,6 +166,13 @@ export default { ...@@ -152,6 +166,13 @@ export default {
</div> </div>
</section> </section>
<canary-ingress
v-if="canRenderCanaryWeight"
class="deploy-board-canary-ingress"
:canary-ingress="deployBoardData.canary_ingress"
@change="changeCanaryWeight"
/>
<section v-if="deployBoardActions" class="deploy-board-actions"> <section v-if="deployBoardActions" class="deploy-board-actions">
<gl-link <gl-link
v-if="deployBoardData.rollback_url" v-if="deployBoardData.rollback_url"
...@@ -177,9 +198,9 @@ export default { ...@@ -177,9 +198,9 @@ export default {
<section v-safe-html="deployBoardSvg" class="deploy-board-empty-state-svg"></section> <section v-safe-html="deployBoardSvg" class="deploy-board-empty-state-svg"></section>
<section class="deploy-board-empty-state-text"> <section class="deploy-board-empty-state-text">
<span class="deploy-board-empty-state-title d-flex"> <span class="deploy-board-empty-state-title d-flex">{{
{{ __('Kubernetes deployment not found') }} __('Kubernetes deployment not found')
</span> }}</span>
<span> <span>
To see deployment progress for your environments, make sure you are deploying to To see deployment progress for your environments, make sure you are deploying to
<code>$KUBE_NAMESPACE</code> and annotating with <code>$KUBE_NAMESPACE</code> and annotating with
......
...@@ -36,3 +36,5 @@ export const CANARY_STATUS = { ...@@ -36,3 +36,5 @@ export const CANARY_STATUS = {
text: __('Canary'), text: __('Canary'),
stable: false, stable: false,
}; };
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
mutation($input: EnvironmentsCanaryIngressUpdateInput!) {
environmentsCanaryIngressUpdate(input: $input) {
errors
}
}
...@@ -32,6 +32,10 @@ ...@@ -32,6 +32,10 @@
width: 100%; width: 100%;
} }
.deploy-board-canary-ingress {
order: 7;
}
.deploy-board-actions { .deploy-board-actions {
order: 3; order: 3;
align-self: center; align-self: center;
...@@ -53,7 +57,6 @@ ...@@ -53,7 +57,6 @@
order: 2; order: 2;
flex-wrap: wrap; flex-wrap: wrap;
margin: auto auto 15px 0; margin: auto auto 15px 0;
} }
.deploy-board-empty-state-title { .deploy-board-empty-state-title {
......
...@@ -7,6 +7,9 @@ module EE ...@@ -7,6 +7,9 @@ module EE
prepended do prepended do
before_action :authorize_create_environment_terminal!, only: [:terminal] before_action :authorize_create_environment_terminal!, only: [:terminal]
before_action do
push_frontend_feature_flag(:canary_ingress_weight_control, default_enabled: true)
end
end end
private private
......
---
title: Allow Users to Set Canary Ingress via UI
merge_request: 49516
author:
type: added
import { mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { CANARY_UPDATE_MODAL } from 'ee/environments/constants';
import CanaryIngress from 'ee/environments/components/canary_ingress.vue';
describe('ee/environments/components/canary_ingress.vue', () => {
let wrapper;
const setWeightTo = (weightWrapper, x) =>
weightWrapper
.findAll(GlDropdownItem)
.at(x / 5)
.vm.$emit('click');
const createComponent = () => {
wrapper = mount(CanaryIngress, {
propsData: {
canaryIngress: {
canary_weight: 60,
},
},
directives: {
GlModal: createMockDirective(),
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
wrapper = null;
});
describe('stable weight', () => {
let stableWeightDropdown;
beforeEach(() => {
stableWeightDropdown = wrapper.find('[data-testid="stable-weight"]');
});
it('displays the current stable weight', () => {
expect(stableWeightDropdown.props('text')).toBe('40');
});
it('emits a change with the new canary weight', () => {
setWeightTo(stableWeightDropdown, 15);
expect(wrapper.emitted('change')).toContainEqual([85]);
});
it('lists options from 0 to 100 in increments of 5', () => {
const options = stableWeightDropdown.findAll(GlDropdownItem);
expect(options).toHaveLength(21);
options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
});
it('is set to open the change modal', () => {
stableWeightDropdown
.findAll(GlDropdownItem)
.wrappers.forEach(w =>
expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
);
});
});
describe('canary weight', () => {
let canaryWeightDropdown;
beforeEach(() => {
canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]');
});
it('displays the current canary weight', () => {
expect(canaryWeightDropdown.props('text')).toBe('60');
});
it('emits a change with the new canary weight', () => {
setWeightTo(canaryWeightDropdown, 15);
expect(wrapper.emitted('change')).toContainEqual([15]);
});
it('lists options from 0 to 100 in increments of 5', () => {
canaryWeightDropdown
.findAll(GlDropdownItem)
.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
});
it('is set to open the change modal', () => {
const options = canaryWeightDropdown.findAll(GlDropdownItem);
expect(options).toHaveLength(21);
options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
});
});
});
import { mount } from '@vue/test-utils';
import { GlAlert, GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import CanaryUpdateModal from 'ee/environments/components/canary_update_modal.vue';
import updateCanaryIngress from 'ee/environments/graphql/mutations/update_canary_ingress.mutation.graphql';
describe('ee/environments/components/canary_update_modal.vue', () => {
let wrapper;
let modal;
let mutate;
const findAlert = () => wrapper.find(GlAlert);
const createComponent = () => {
mutate = jest.fn().mockResolvedValue();
wrapper = mount(CanaryUpdateModal, {
propsData: {
environment: {
name: 'staging',
global_id: 'gid://environments/staging',
},
weight: 60,
visible: true,
},
mocks: {
$apollo: { mutate },
},
});
modal = wrapper.find(GlModal);
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
wrapper = null;
});
beforeEach(() => {
createComponent();
});
it('should bind the modal props', () => {
expect(modal.props()).toMatchObject({
modalId: 'confirm-canary-change',
actionPrimary: {
text: 'Change ratio',
attributes: [{ variant: 'info' }],
},
actionCancel: { text: 'Cancel' },
});
});
it('should display the new weights', () => {
expect(modal.text()).toContain('Stable: 40');
expect(modal.text()).toContain('Canary: 60');
});
it('should display the affected environment', () => {
expect(modal.text()).toContain(
'You are changing the ratio of the canary rollout for staging compared to the stable deployment to:',
);
});
it('should update the weight on primary action', () => {
modal.vm.$emit('primary');
expect(mutate).toHaveBeenCalledWith({
mutation: updateCanaryIngress,
variables: {
input: {
id: 'gid://environments/staging',
weight: 60,
},
},
});
});
it('should do nothing on cancel', () => {
modal.vm.$emit('secondary');
expect(mutate).not.toHaveBeenCalled();
});
it('should not display an error if there was not one', async () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: [] } } });
modal.vm.$emit('primary');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
});
it('should display an error if there was one', async () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } });
modal.vm.$emit('primary');
await wrapper.vm.$nextTick();
expect(findAlert().text()).toBe('error');
});
it('should display a generic error if there was a top-level one', async () => {
mutate.mockRejectedValue();
modal.vm.$emit('primary');
await waitForPromises();
await wrapper.vm.$nextTick();
expect(findAlert().text()).toBe('Something went wrong. Please try again later');
});
it('hides teh alert on dismiss', async () => {
mutate.mockResolvedValue({ data: { environmentsCanaryIngressUpdate: { errors: ['error'] } } });
modal.vm.$emit('primary');
await wrapper.vm.$nextTick();
const alert = findAlert();
alert.vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(alert.exists()).toBe(false);
});
});
...@@ -2,6 +2,7 @@ import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui'; ...@@ -2,6 +2,7 @@ import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue'; import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryIngress from 'ee/environments/components/canary_ingress.vue';
import { deployBoardMockData, environment } from './mock_data'; import { deployBoardMockData, environment } from './mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`; const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
...@@ -11,6 +12,7 @@ describe('Deploy Board', () => { ...@@ -11,6 +12,7 @@ describe('Deploy Board', () => {
const createComponent = (props = {}) => const createComponent = (props = {}) =>
mount(Vue.extend(DeployBoard), { mount(Vue.extend(DeployBoard), {
provide: { glFeatures: { canaryIngressWeightControl: true } },
propsData: { propsData: {
deployBoardData: deployBoardMockData, deployBoardData: deployBoardMockData,
isLoading: false, isLoading: false,
...@@ -62,6 +64,12 @@ describe('Deploy Board', () => { ...@@ -62,6 +64,12 @@ describe('Deploy Board', () => {
expect(tooltip.props('target')()).toBe(iconSpan.element); expect(tooltip.props('target')()).toBe(iconSpan.element);
expect(icon.props('name')).toBe('question'); expect(icon.props('name')).toBe('question');
}); });
it('renders the canary weight selector', () => {
const canary = wrapper.find(CanaryIngress);
expect(canary.exists()).toBe(true);
expect(canary.props('canaryIngress')).toEqual({ canary_weight: 50 });
});
}); });
describe('with empty state', () => { describe('with empty state', () => {
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import EnvironmentAlert from 'ee/environments/components/environment_alert.vue'; import EnvironmentAlert from 'ee/environments/components/environment_alert.vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryUpdateModal from 'ee/environments/components/canary_update_modal.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue'; import EnvironmentTable from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub'; import eventHub from '~/environments/event_hub';
import { deployBoardMockData } from './mock_data'; import { deployBoardMockData } from './mock_data';
...@@ -91,6 +93,7 @@ describe('Environment table', () => { ...@@ -91,6 +93,7 @@ describe('Environment table', () => {
rollback_url: 'url', rollback_url: 'url',
completion: 100, completion: 100,
is_completed: true, is_completed: true,
canary_ingress: { canary_weight: 60 },
}, },
isDeployBoardVisible: false, isDeployBoardVisible: false,
}; };
...@@ -169,4 +172,40 @@ describe('Environment table', () => { ...@@ -169,4 +172,40 @@ describe('Environment table', () => {
expect(wrapper.find(EnvironmentAlert).exists()).toBe(true); expect(wrapper.find(EnvironmentAlert).exists()).toBe(true);
}); });
it('should set the enviornment to change and weight when a change canary weight event is recevied', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40);
await wrapper.vm.$nextTick();
expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({
weight: 40,
environment: mockItem,
});
});
}); });
...@@ -32,6 +32,9 @@ export const deployBoardMockData = { ...@@ -32,6 +32,9 @@ export const deployBoardMockData = {
rollback_url: 'url', rollback_url: 'url',
completion: 100, completion: 100,
status: 'found', status: 'found',
canary_ingress: {
canary_weight: 50,
},
}; };
export const environment = { export const environment = {
......
...@@ -4996,6 +4996,30 @@ msgstr "" ...@@ -4996,6 +4996,30 @@ msgstr ""
msgid "Canary weight must be specified and valid range (0..100)." msgid "Canary weight must be specified and valid range (0..100)."
msgstr "" msgstr ""
msgid "CanaryIngress|%{boldStart}Canary:%{boldEnd} %{canary}"
msgstr ""
msgid "CanaryIngress|%{boldStart}Stable:%{boldEnd} %{stable}"
msgstr ""
msgid "CanaryIngress|Canary"
msgstr ""
msgid "CanaryIngress|Change ratio"
msgstr ""
msgid "CanaryIngress|Change the ratio of canary deployments?"
msgstr ""
msgid "CanaryIngress|Doing so will set a deployment change in progress. This temporarily blocks any further configuration until the deployment is finished."
msgstr ""
msgid "CanaryIngress|Stable"
msgstr ""
msgid "CanaryIngress|You are changing the ratio of the canary rollout for %{environment} compared to the stable deployment to:"
msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
...@@ -25937,6 +25961,9 @@ msgstr "" ...@@ -25937,6 +25961,9 @@ msgstr ""
msgid "Something went wrong, unable to search projects" msgid "Something went wrong, unable to search projects"
msgstr "" msgstr ""
msgid "Something went wrong. Please try again later"
msgstr ""
msgid "Something went wrong. Please try again." msgid "Something went wrong. Please try again."
msgstr "" msgstr ""
......
...@@ -454,10 +454,10 @@ RSpec.describe 'Environments page', :js do ...@@ -454,10 +454,10 @@ RSpec.describe 'Environments page', :js do
expect(page).to have_content 'review-1' expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2' expect(page).to have_content 'review-2'
within('.ci-table') do within('.ci-table') do
within('.gl-responsive-table-row:nth-child(3)') do within('[data-qa-selector="environment_item"]', text: 'review-1') do
expect(find('.js-auto-stop').text).not_to be_empty expect(find('.js-auto-stop').text).not_to be_empty
end end
within('.gl-responsive-table-row:nth-child(4)') do within('[data-qa-selector="environment_item"]', text: 'review-2') do
expect(find('.js-auto-stop').text).not_to be_empty expect(find('.js-auto-stop').text).not_to be_empty
end end
end end
......
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