Commit 7214a994 authored by Andrew Fontaine's avatar Andrew Fontaine

Integrate deploy boards into new environments page

This includes the setting of the canary ingress weight. Some minor
checks are needed to send the environment to the canary weight modal and
to ensure that camel case is used for the deploy board properties when
we are in new environment land.
parent 4e941c21
...@@ -17,6 +17,11 @@ export default { ...@@ -17,6 +17,11 @@ export default {
required: true, required: true,
type: Object, type: Object,
}, },
graphql: {
required: false,
type: Boolean,
default: false,
},
}, },
ingressOptions: Array(100 / 5 + 1) ingressOptions: Array(100 / 5 + 1)
.fill(0) .fill(0)
...@@ -47,11 +52,17 @@ export default { ...@@ -47,11 +52,17 @@ export default {
canaryWeightId() { canaryWeightId() {
return uniqueId('canary-weight-'); return uniqueId('canary-weight-');
}, },
weight() {
if (this.graphql) {
return this.canaryIngress.canaryWeight;
}
return this.canaryIngress.canary_weight;
},
stableWeight() { stableWeight() {
return (100 - this.canaryIngress.canary_weight).toString(); return (100 - this.weight).toString();
}, },
canaryWeight() { canaryWeight() {
return this.canaryIngress.canary_weight.toString(); return this.weight.toString();
}, },
}, },
methods: { methods: {
......
...@@ -71,7 +71,7 @@ export default { ...@@ -71,7 +71,7 @@ export default {
mutation: updateCanaryIngress, mutation: updateCanaryIngress,
variables: { variables: {
input: { input: {
id: this.environment.global_id, id: this.environment.global_id || this.environment.globalId,
weight: this.weight, weight: this.weight,
}, },
}, },
......
<script> <script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
/** /**
* Renders a deploy board. * Renders a deploy board.
* *
...@@ -17,11 +16,11 @@ import { ...@@ -17,11 +16,11 @@ import {
GlTooltip, GlTooltip,
GlTooltipDirective, GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml, GlSafeHtmlDirective as SafeHtml,
GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { n__ } from '~/locale'; import { s__, n__ } from '~/locale';
import instanceComponent from '~/vue_shared/components/deployment_instance.vue'; import instanceComponent from '~/vue_shared/components/deployment_instance.vue';
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'; import CanaryIngress from './canary_ingress.vue';
...@@ -32,13 +31,13 @@ export default { ...@@ -32,13 +31,13 @@ export default {
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
GlLink, GlLink,
GlSprintf,
GlTooltip, GlTooltip,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
SafeHtml, SafeHtml,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
deployBoardData: { deployBoardData: {
type: Object, type: Object,
...@@ -57,6 +56,11 @@ export default { ...@@ -57,6 +56,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
graphql: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
canRenderDeployBoard() { canRenderDeployBoard() {
...@@ -65,8 +69,15 @@ export default { ...@@ -65,8 +69,15 @@ export default {
canRenderEmptyState() { canRenderEmptyState() {
return this.isEmpty; return this.isEmpty;
}, },
canaryIngress() {
if (this.graphql) {
return this.deployBoardData.canaryIngress;
}
return this.deployBoardData.canary_ingress;
},
canRenderCanaryWeight() { canRenderCanaryWeight() {
return !isEmpty(this.deployBoardData.canary_ingress); return !isEmpty(this.canaryIngress);
}, },
instanceCount() { instanceCount() {
const { instances } = this.deployBoardData; const { instances } = this.deployBoardData;
...@@ -90,8 +101,20 @@ export default { ...@@ -90,8 +101,20 @@ export default {
deployBoardSvg() { deployBoardSvg() {
return deployBoardSvg; return deployBoardSvg;
}, },
rollbackUrl() {
if (this.graphql) {
return this.deployBoardData.rollbackUrl;
}
return this.deployBoardData.rollback_url;
},
abortUrl() {
if (this.graphql) {
return this.deployBoardData.abortUrl;
}
return this.deployBoardData.abort_url;
},
deployBoardActions() { deployBoardActions() {
return this.deployBoardData.rollback_url || this.deployBoardData.abort_url; return this.rollbackUrl || this.abortUrl;
}, },
statuses() { statuses() {
// Canary is not a pod status but it needs to be in the legend. // Canary is not a pod status but it needs to be in the legend.
...@@ -106,7 +129,17 @@ export default { ...@@ -106,7 +129,17 @@ export default {
changeCanaryWeight(weight) { changeCanaryWeight(weight) {
this.$emit('changeCanaryWeight', weight); this.$emit('changeCanaryWeight', weight);
}, },
podName(instance) {
if (this.graphql) {
return instance.podName;
}
return instance.pod_name;
}, },
},
emptyStateText: s__(
'DeployBoards|To see deployment progress for your environments, make sure you are deploying to %{codeStart}$KUBE_NAMESPACE%{codeEnd} and annotating with %{codeStart}app.gitlab.com/app=$CI_PROJECT_PATH_SLUG%{codeEnd} and %{codeStart}app.gitlab.com/env=$CI_ENVIRONMENT_SLUG%{codeEnd}.',
),
}; };
</script> </script>
<template> <template>
...@@ -152,7 +185,7 @@ export default { ...@@ -152,7 +185,7 @@ export default {
: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="podName(instance)"
:logs-path="logsPath" :logs-path="logsPath"
:stable="instance.stable" :stable="instance.stable"
/> />
...@@ -163,22 +196,23 @@ export default { ...@@ -163,22 +196,23 @@ export default {
<canary-ingress <canary-ingress
v-if="canRenderCanaryWeight" v-if="canRenderCanaryWeight"
class="deploy-board-canary-ingress" class="deploy-board-canary-ingress"
:canary-ingress="deployBoardData.canary_ingress" :canary-ingress="canaryIngress"
:graphql="graphql"
@change="changeCanaryWeight" @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="rollbackUrl"
:href="deployBoardData.rollback_url" :href="rollbackUrl"
class="btn" class="btn"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
>{{ __('Rollback') }}</gl-link >{{ __('Rollback') }}</gl-link
> >
<gl-link <gl-link
v-if="deployBoardData.abort_url" v-if="abortUrl"
:href="deployBoardData.abort_url" :href="abortUrl"
class="btn btn-danger btn-inverted" class="btn btn-danger btn-inverted"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
...@@ -196,11 +230,11 @@ export default { ...@@ -196,11 +230,11 @@ export default {
__('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 <gl-sprintf :message="$options.emptyStateText">
<code>$KUBE_NAMESPACE</code> and annotating with <template #code="{ content }">
<code>app.gitlab.com/app=$CI_PROJECT_PATH_SLUG</code> <code>{{ content }}</code>
and </template>
<code>app.gitlab.com/env=$CI_ENVIRONMENT_SLUG</code>. </gl-sprintf>
</span> </span>
</section> </section>
</div> </div>
......
<script>
import { GlCollapse, GlButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import setEnvironmentToChangeCanaryMutation from '../graphql/mutations/set_environment_to_change_canary.mutation.graphql';
import DeployBoard from './deploy_board.vue';
export default {
components: {
DeployBoard,
GlButton,
GlCollapse,
},
props: {
rolloutStatus: {
required: true,
type: Object,
},
environment: {
required: true,
type: Object,
},
},
data() {
return { visible: false };
},
computed: {
icon() {
return this.visible ? 'angle-down' : 'angle-right';
},
label() {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
isLoading() {
return this.rolloutStatus.status === 'loading';
},
isEmpty() {
return this.rolloutStatus.status === 'not_found';
},
},
methods: {
toggleCollapse() {
this.visible = !this.visible;
},
changeCanaryWeight(weight) {
this.$apollo.mutate({
mutation: setEnvironmentToChangeCanaryMutation,
variables: {
environment: this.environment,
weight,
},
});
},
},
i18n: {
collapse: __('Collapse'),
expand: __('Expand'),
pods: s__('DeployBoard|Kubernetes Pods'),
},
};
</script>
<template>
<div>
<div>
<gl-button
class="gl-mr-4 gl-min-w-fit-content"
:icon="icon"
:aria-label="label"
size="small"
category="tertiary"
@click="toggleCollapse"
/>
<span>{{ $options.i18n.pods }}</span>
</div>
<gl-collapse :visible="visible">
<deploy-board
:deploy-board-data="rolloutStatus"
:is-loading="isLoading"
:is-empty="isEmpty"
:environment="environment"
graphql
class="gl-reset-bg!"
@changeCanaryWeight="changeCanaryWeight"
/>
</gl-collapse>
</div>
</template>
...@@ -20,6 +20,7 @@ import Monitoring from './environment_monitoring.vue'; ...@@ -20,6 +20,7 @@ import Monitoring from './environment_monitoring.vue';
import Terminal from './environment_terminal_button.vue'; import Terminal from './environment_terminal_button.vue';
import Delete from './environment_delete.vue'; import Delete from './environment_delete.vue';
import Deployment from './deployment.vue'; import Deployment from './deployment.vue';
import DeployBoardWrapper from './deploy_board_wrapper.vue';
export default { export default {
components: { components: {
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
GlSprintf, GlSprintf,
Actions, Actions,
Deployment, Deployment,
DeployBoardWrapper,
ExternalUrl, ExternalUrl,
StopComponent, StopComponent,
Rollback, Rollback,
...@@ -145,6 +147,9 @@ export default { ...@@ -145,6 +147,9 @@ export default {
displayName() { displayName() {
return truncate(this.name, 80); return truncate(this.name, 80);
}, },
rolloutStatus() {
return this.environment?.rolloutStatus;
},
}, },
methods: { methods: {
toggleCollapse() { toggleCollapse() {
...@@ -159,6 +164,14 @@ export default { ...@@ -159,6 +164,14 @@ export default {
'gl-md-pl-7', 'gl-md-pl-7',
'gl-bg-gray-10', 'gl-bg-gray-10',
], ],
deployBoardClasses: [
'gl-border-gray-100',
'gl-border-t-solid',
'gl-border-1',
'gl-py-4',
'gl-md-pl-7',
'gl-bg-gray-10',
],
}; };
</script> </script>
<template> <template>
...@@ -298,6 +311,14 @@ export default { ...@@ -298,6 +311,14 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
<div v-if="rolloutStatus" :class="$options.deployBoardClasses">
<deploy-board-wrapper
:rollout-status="rolloutStatus"
:environment="environment"
:class="{ 'gl-ml-7': inFolder }"
class="gl-pl-4"
/>
</div>
</gl-collapse> </gl-collapse>
</div> </div>
</template> </template>
...@@ -8,16 +8,19 @@ import pageInfoQuery from '../graphql/queries/page_info.query.graphql'; ...@@ -8,16 +8,19 @@ import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql'; import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql'; import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql'; import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
import environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue'; import EnvironmentFolder from './new_environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue'; import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue'; import StopEnvironmentModal from './stop_environment_modal.vue';
import EnvironmentItem from './new_environment_item.vue'; import EnvironmentItem from './new_environment_item.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue'; import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue'; import DeleteEnvironmentModal from './delete_environment_modal.vue';
import CanaryUpdateModal from './canary_update_modal.vue';
export default { export default {
components: { components: {
DeleteEnvironmentModal, DeleteEnvironmentModal,
CanaryUpdateModal,
ConfirmRollbackModal, ConfirmRollbackModal,
EnvironmentFolder, EnvironmentFolder,
EnableReviewAppModal, EnableReviewAppModal,
...@@ -56,6 +59,12 @@ export default { ...@@ -56,6 +59,12 @@ export default {
environmentToStop: { environmentToStop: {
query: environmentToStopQuery, query: environmentToStopQuery,
}, },
environmentToChangeCanary: {
query: environmentToChangeCanaryQuery,
},
weight: {
query: environmentToChangeCanaryQuery,
},
}, },
inject: ['newEnvironmentPath', 'canCreateEnvironment'], inject: ['newEnvironmentPath', 'canCreateEnvironment'],
i18n: { i18n: {
...@@ -80,6 +89,8 @@ export default { ...@@ -80,6 +89,8 @@ export default {
environmentToDelete: {}, environmentToDelete: {},
environmentToRollback: {}, environmentToRollback: {},
environmentToStop: {}, environmentToStop: {},
environmentToChangeCanary: {},
weight: 0,
}; };
}, },
computed: { computed: {
...@@ -186,6 +197,7 @@ export default { ...@@ -186,6 +197,7 @@ export default {
<delete-environment-modal :environment="environmentToDelete" graphql /> <delete-environment-modal :environment="environmentToDelete" graphql />
<stop-environment-modal :environment="environmentToStop" graphql /> <stop-environment-modal :environment="environmentToStop" graphql />
<confirm-rollback-modal :environment="environmentToRollback" graphql /> <confirm-rollback-modal :environment="environmentToRollback" graphql />
<canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
<gl-tabs <gl-tabs
:action-secondary="addEnvironment" :action-secondary="addEnvironment"
:action-primary="openReviewAppModal" :action-primary="openReviewAppModal"
......
mutation SetEnvironmentToChangeCanary($environment: LocalEnvironmentInput, $weight: Int!) {
setEnvironmentToChangeCanary(environment: $environment, weight: $weight) @client
}
query environmentToChangeCanary {
environmentToChangeCanary @client
weight @client
}
...@@ -10,6 +10,7 @@ import pollIntervalQuery from './queries/poll_interval.query.graphql'; ...@@ -10,6 +10,7 @@ import pollIntervalQuery from './queries/poll_interval.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
import environmentToStopQuery from './queries/environment_to_stop.query.graphql'; import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql'; import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
import environmentToChangeCanaryQuery from './queries/environment_to_change_canary.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql'; import pageInfoQuery from './queries/page_info.query.graphql';
const buildErrors = (errors = []) => ({ const buildErrors = (errors = []) => ({
...@@ -134,6 +135,12 @@ export const resolvers = (endpoint) => ({ ...@@ -134,6 +135,12 @@ export const resolvers = (endpoint) => ({
data: { environmentToRollback: environment }, data: { environmentToRollback: environment },
}); });
}, },
setEnvironmentToChangeCanary(_, { environment, weight }, { client }) {
client.writeQuery({
query: environmentToChangeCanaryQuery,
data: { environmentToChangeCanary: environment, weight },
});
},
cancelAutoStop(_, { autoStopUrl }) { cancelAutoStop(_, { autoStopUrl }) {
return axios return axios
.post(autoStopUrl) .post(autoStopUrl)
......
...@@ -81,5 +81,6 @@ extend type Mutation { ...@@ -81,5 +81,6 @@ extend type Mutation {
setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToChangeCanary(environment: LocalEnvironmentInput, weight: Int): LocalErrors
action(environment: LocalEnvironmentInput): LocalErrors action(environment: LocalEnvironmentInput): LocalErrors
} }
- page_title _("Environments") - page_title _("Environments")
- add_page_specific_style 'page_bundles/environments'
- if Feature.enabled?(:new_environments_table) - if Feature.enabled?(:new_environments_table)
#environments-table{ data: { endpoint: project_environments_path(@project, format: :json), #environments-table{ data: { endpoint: project_environments_path(@project, format: :json),
...@@ -9,7 +10,6 @@ ...@@ -9,7 +10,6 @@
"project-path" => @project.full_path, "project-path" => @project.full_path,
"default-branch-name" => @project.default_branch_or_main } } "default-branch-name" => @project.default_branch_or_main } }
- else - else
- add_page_specific_style 'page_bundles/environments'
#environments-list-view{ data: { environments_data: environments_list_data, #environments-list-view{ data: { environments_data: environments_list_data,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s, "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
......
...@@ -11948,6 +11948,12 @@ msgstr "" ...@@ -11948,6 +11948,12 @@ msgstr ""
msgid "Deploy to..." msgid "Deploy to..."
msgstr "" msgstr ""
msgid "DeployBoards|To see deployment progress for your environments, make sure you are deploying to %{codeStart}$KUBE_NAMESPACE%{codeEnd} and annotating with %{codeStart}app.gitlab.com/app=$CI_PROJECT_PATH_SLUG%{codeEnd} and %{codeStart}app.gitlab.com/env=$CI_ENVIRONMENT_SLUG%{codeEnd}."
msgstr ""
msgid "DeployBoard|Kubernetes Pods"
msgstr ""
msgid "DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}" msgid "DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}"
msgstr "" msgstr ""
......
...@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import CanaryIngress from '~/environments/components/canary_ingress.vue'; import CanaryIngress from '~/environments/components/canary_ingress.vue';
import { CANARY_UPDATE_MODAL } from '~/environments/constants'; import { CANARY_UPDATE_MODAL } from '~/environments/constants';
import { rolloutStatus } from './graphql/mock_data';
describe('/environments/components/canary_ingress.vue', () => { describe('/environments/components/canary_ingress.vue', () => {
let wrapper; let wrapper;
...@@ -13,16 +14,18 @@ describe('/environments/components/canary_ingress.vue', () => { ...@@ -13,16 +14,18 @@ describe('/environments/components/canary_ingress.vue', () => {
.at(x / 5) .at(x / 5)
.vm.$emit('click'); .vm.$emit('click');
const createComponent = () => { const createComponent = (props = {}, options = {}) => {
wrapper = mount(CanaryIngress, { wrapper = mount(CanaryIngress, {
propsData: { propsData: {
canaryIngress: { canaryIngress: {
canary_weight: 60, canary_weight: 60,
}, },
...props,
}, },
directives: { directives: {
GlModal: createMockDirective(), GlModal: createMockDirective(),
}, },
...options,
}); });
}; };
...@@ -94,9 +97,25 @@ describe('/environments/components/canary_ingress.vue', () => { ...@@ -94,9 +97,25 @@ describe('/environments/components/canary_ingress.vue', () => {
}); });
it('is set to open the change modal', () => { it('is set to open the change modal', () => {
const options = canaryWeightDropdown.findAll(GlDropdownItem); canaryWeightDropdown
expect(options).toHaveLength(21); .findAll(GlDropdownItem)
options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString())); .wrappers.forEach((w) =>
expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
);
});
});
describe('graphql', () => {
beforeEach(() => {
createComponent({
graphql: true,
canaryIngress: rolloutStatus.canaryIngress,
});
});
it('shows the correct weight', () => {
const canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]');
expect(canaryWeightDropdown.props('text')).toBe('50');
}); });
}); });
}); });
...@@ -4,6 +4,7 @@ import Vue, { nextTick } from 'vue'; ...@@ -4,6 +4,7 @@ import Vue, { nextTick } from 'vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue'; import CanaryIngress from '~/environments/components/canary_ingress.vue';
import DeployBoard from '~/environments/components/deploy_board.vue'; import DeployBoard from '~/environments/components/deploy_board.vue';
import { deployBoardMockData, environment } from './mock_data'; import { deployBoardMockData, environment } from './mock_data';
import { rolloutStatus } from './graphql/mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`; const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
...@@ -28,7 +29,7 @@ describe('Deploy Board', () => { ...@@ -28,7 +29,7 @@ describe('Deploy Board', () => {
}); });
it('should render percentage with completion value provided', () => { it('should render percentage with completion value provided', () => {
expect(wrapper.vm.$refs.percentage.innerText).toEqual(`${deployBoardMockData.completion}%`); expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${deployBoardMockData.completion}%`);
}); });
it('should render total instance count', () => { it('should render total instance count', () => {
...@@ -57,20 +58,74 @@ describe('Deploy Board', () => { ...@@ -57,20 +58,74 @@ describe('Deploy Board', () => {
it('sets up a tooltip for the legend', () => { it('sets up a tooltip for the legend', () => {
const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]'); const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]');
const tooltip = wrapper.find(GlTooltip); const tooltip = wrapper.findComponent(GlTooltip);
const icon = iconSpan.find(GlIcon); const icon = iconSpan.findComponent(GlIcon);
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', () => { it('renders the canary weight selector', () => {
const canary = wrapper.find(CanaryIngress); const canary = wrapper.findComponent(CanaryIngress);
expect(canary.exists()).toBe(true); expect(canary.exists()).toBe(true);
expect(canary.props('canaryIngress')).toEqual({ canary_weight: 50 }); expect(canary.props('canaryIngress')).toEqual({ canary_weight: 50 });
}); });
}); });
describe('with new valid data', () => {
beforeEach(async () => {
wrapper = createComponent({
graphql: true,
deployBoardData: rolloutStatus,
});
await nextTick();
});
it('should render percentage with completion value provided', () => {
expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${rolloutStatus.completion}%`);
});
it('should render total instance count', () => {
const renderedTotal = wrapper.find('.deploy-board-instances-text');
const actualTotal = rolloutStatus.instances.length;
const output = `${actualTotal > 1 ? 'Instances' : 'Instance'} (${actualTotal})`;
expect(renderedTotal.text()).toEqual(output);
});
it('should render all instances', () => {
const instances = wrapper.findAll('.deploy-board-instances-container a');
expect(instances).toHaveLength(rolloutStatus.instances.length);
expect(
instances.at(1).classes(`deployment-instance-${rolloutStatus.instances[2].status}`),
).toBe(true);
});
it('should render an abort and a rollback button with the provided url', () => {
const buttons = wrapper.findAll('.deploy-board-actions a');
expect(buttons.at(0).attributes('href')).toEqual(rolloutStatus.rollbackUrl);
expect(buttons.at(1).attributes('href')).toEqual(rolloutStatus.abortUrl);
});
it('sets up a tooltip for the legend', () => {
const iconSpan = wrapper.find('[data-testid="legend-tooltip-target"]');
const tooltip = wrapper.findComponent(GlTooltip);
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
expect(icon.props('name')).toBe('question');
});
it('renders the canary weight selector', () => {
const canary = wrapper.findComponent(CanaryIngress);
expect(canary.exists()).toBe(true);
expect(canary.props('canaryIngress')).toEqual({ canaryWeight: 50 });
expect(canary.props('graphql')).toBe(true);
});
});
describe('with empty state', () => { describe('with empty state', () => {
beforeEach((done) => { beforeEach((done) => {
wrapper = createComponent({ wrapper = createComponent({
...@@ -102,7 +157,7 @@ describe('Deploy Board', () => { ...@@ -102,7 +157,7 @@ describe('Deploy Board', () => {
}); });
it('should render loading spinner', () => { it('should render loading spinner', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
}); });
}); });
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlCollapse, GlIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
import createMockApollo from 'helpers/mock_apollo_helper';
import { __, s__ } from '~/locale';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import setEnvironmentToChangeCanaryMutation from '~/environments/graphql/mutations/set_environment_to_change_canary.mutation.graphql';
import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
Vue.use(VueApollo);
describe('~/environments/components/deploy_board_wrapper.vue', () => {
let wrapper;
let mockApollo;
const findDeployBoard = () => wrapper.findComponent(DeployBoard);
const createWrapper = ({ propsData = {} } = {}) => {
mockApollo = createMockApollo();
return mountExtended(DeployBoardWrapper, {
propsData: { environment: resolvedEnvironment, rolloutStatus, ...propsData },
provide: { helpPagePath: '/help' },
stubs: { transition: stubTransition() },
apolloProvider: mockApollo,
});
};
const expandCollapsedSection = async () => {
const button = wrapper.findByRole('button', { name: __('Expand') });
await button.trigger('click');
return button;
};
afterEach(() => {
wrapper?.destroy();
});
it('is labeled Kubernetes Pods', () => {
wrapper = createWrapper();
expect(wrapper.findByText(s__('DeployBoard|Kubernetes Pods')).exists()).toBe(true);
});
describe('collapse', () => {
let icon;
let collapse;
beforeEach(() => {
wrapper = createWrapper();
collapse = wrapper.findComponent(GlCollapse);
icon = wrapper.findComponent(GlIcon);
});
it('is collapsed by default', () => {
expect(collapse.attributes('visible')).toBeUndefined();
expect(icon.props('name')).toBe('angle-right');
});
it('opens on click', async () => {
const button = await expandCollapsedSection();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
expect(icon.props('name')).toBe('angle-down');
const deployBoard = findDeployBoard();
expect(deployBoard.exists()).toBe(true);
});
});
describe('deploy board', () => {
it('passes the rollout status on and sets graphql to true', async () => {
wrapper = createWrapper();
await expandCollapsedSection();
const deployBoard = findDeployBoard();
expect(deployBoard.props('deployBoardData')).toEqual(rolloutStatus);
expect(deployBoard.props('graphql')).toBe(true);
});
it('sets the update to the canary via graphql', () => {
wrapper = createWrapper();
jest.spyOn(mockApollo.defaultClient, 'mutate');
const deployBoard = findDeployBoard();
deployBoard.vm.$emit('changeCanaryWeight', 15);
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToChangeCanaryMutation,
variables: { environment: resolvedEnvironment, weight: 15 },
});
});
describe('is loading', () => {
it('should set the loading prop', async () => {
wrapper = createWrapper({
propsData: { rolloutStatus: { ...rolloutStatus, status: 'loading' } },
});
await expandCollapsedSection();
const deployBoard = findDeployBoard();
expect(deployBoard.props('isLoading')).toBe(true);
});
});
describe('is empty', () => {
it('should set the empty prop', async () => {
wrapper = createWrapper({
propsData: { rolloutStatus: { ...rolloutStatus, status: 'not_found' } },
});
await expandCollapsedSection();
const deployBoard = findDeployBoard();
expect(deployBoard.props('isEmpty')).toBe(true);
});
});
});
});
export const rolloutStatus = {
instances: [
{
status: 'succeeded',
tooltip: 'tanuki-2334 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{
status: 'succeeded',
tooltip: 'tanuki-2335 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{
status: 'succeeded',
tooltip: 'tanuki-2336 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{
status: 'succeeded',
tooltip: 'tanuki-2337 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{
status: 'succeeded',
tooltip: 'tanuki-2338 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{
status: 'succeeded',
tooltip: 'tanuki-2339 Finished',
podName: 'production-tanuki-1',
stable: false,
},
{ status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2334 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2335 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2336 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2337 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2338 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2339 Finished', podName: 'production-tanuki-1' },
{ status: 'succeeded', tooltip: 'tanuki-2340 Finished', podName: 'production-tanuki-1' },
{ status: 'running', tooltip: 'tanuki-2341 Deploying', podName: 'production-tanuki-1' },
{ status: 'running', tooltip: 'tanuki-2342 Deploying', podName: 'production-tanuki-1' },
{ status: 'running', tooltip: 'tanuki-2343 Deploying', podName: 'production-tanuki-1' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed', podName: 'production-tanuki-1' },
{ status: 'unknown', tooltip: 'tanuki-2345 Ready', podName: 'production-tanuki-1' },
{ status: 'unknown', tooltip: 'tanuki-2346 Ready', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2348 Preparing', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2349 Preparing', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2350 Preparing', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2353 Preparing', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2354 waiting', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2355 waiting', podName: 'production-tanuki-1' },
{ status: 'pending', tooltip: 'tanuki-2356 waiting', podName: 'production-tanuki-1' },
],
abortUrl: 'url',
rollbackUrl: 'url',
completion: 100,
status: 'found',
canaryIngress: { canaryWeight: 50 },
};
export const environmentsApp = { export const environmentsApp = {
environments: [ environments: [
{ {
......
...@@ -8,7 +8,8 @@ import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; ...@@ -8,7 +8,8 @@ import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue'; import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import Deployment from '~/environments/components/deployment.vue'; import Deployment from '~/environments/components/deployment.vue';
import { resolvedEnvironment } from './graphql/mock_data'; import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -455,4 +456,35 @@ describe('~/environments/components/new_environment_item.vue', () => { ...@@ -455,4 +456,35 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(emptyState.exists()).toBe(false); expect(emptyState.exists()).toBe(false);
}); });
}); });
describe('deploy boards', () => {
it('should show a deploy board if the environment has a rollout status', async () => {
const environment = {
...resolvedEnvironment,
rolloutStatus,
};
wrapper = createWrapper({
propsData: { environment },
apolloProvider: createApolloProvider(),
});
await expandCollapsedSection();
const deployBoard = wrapper.findComponent(DeployBoardWrapper);
expect(deployBoard.exists()).toBe(true);
expect(deployBoard.props('rolloutStatus')).toBe(rolloutStatus);
});
it('should not show a deploy board if the environment has no rollout status', async () => {
wrapper = createWrapper({
apolloProvider: createApolloProvider(),
});
await expandCollapsedSection();
const deployBoard = wrapper.findComponent(DeployBoardWrapper);
expect(deployBoard.exists()).toBe(false);
});
});
}); });
...@@ -10,6 +10,7 @@ import EnvironmentsApp from '~/environments/components/new_environments_app.vue' ...@@ -10,6 +10,7 @@ import EnvironmentsApp from '~/environments/components/new_environments_app.vue'
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import EnvironmentsItem from '~/environments/components/new_environment_item.vue'; import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data'; import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -20,6 +21,8 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -20,6 +21,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
let environmentFolderMock; let environmentFolderMock;
let paginationMock; let paginationMock;
let environmentToStopMock; let environmentToStopMock;
let environmentToChangeCanaryMock;
let weightMock;
const createApolloProvider = () => { const createApolloProvider = () => {
const mockResolvers = { const mockResolvers = {
...@@ -30,6 +33,8 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -30,6 +33,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentToStop: environmentToStopMock, environmentToStop: environmentToStopMock,
environmentToDelete: jest.fn().mockResolvedValue(resolvedEnvironment), environmentToDelete: jest.fn().mockResolvedValue(resolvedEnvironment),
environmentToRollback: jest.fn().mockResolvedValue(resolvedEnvironment), environmentToRollback: jest.fn().mockResolvedValue(resolvedEnvironment),
environmentToChangeCanary: environmentToChangeCanaryMock,
weight: weightMock,
}, },
}; };
...@@ -53,6 +58,8 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -53,6 +58,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentsApp, environmentsApp,
folder, folder,
environmentToStop = {}, environmentToStop = {},
environmentToChangeCanary = {},
weight = 0,
pageInfo = { pageInfo = {
total: 20, total: 20,
perPage: 5, perPage: 5,
...@@ -67,6 +74,8 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -67,6 +74,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentFolderMock.mockReturnValue(folder); environmentFolderMock.mockReturnValue(folder);
paginationMock.mockReturnValue(pageInfo); paginationMock.mockReturnValue(pageInfo);
environmentToStopMock.mockReturnValue(environmentToStop); environmentToStopMock.mockReturnValue(environmentToStop);
environmentToChangeCanaryMock.mockReturnValue(environmentToChangeCanary);
weightMock.mockReturnValue(weight);
const apolloProvider = createApolloProvider(); const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide }); wrapper = createWrapper({ apolloProvider, provide });
...@@ -78,6 +87,8 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -78,6 +87,8 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentAppMock = jest.fn(); environmentAppMock = jest.fn();
environmentFolderMock = jest.fn(); environmentFolderMock = jest.fn();
environmentToStopMock = jest.fn(); environmentToStopMock = jest.fn();
environmentToChangeCanaryMock = jest.fn();
weightMock = jest.fn();
paginationMock = jest.fn(); paginationMock = jest.fn();
}); });
...@@ -207,6 +218,19 @@ describe('~/environments/components/new_environments_app.vue', () => { ...@@ -207,6 +218,19 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(modal.props('environment')).toMatchObject(resolvedEnvironment); expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
}); });
it('should pass the environment to change canary to the canary update modal', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
environmentToChangeCanary: resolvedEnvironment,
weight: 10,
});
const modal = wrapper.findComponent(CanaryUpdateModal);
expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
});
}); });
describe('pagination', () => { describe('pagination', () => {
......
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