diff options
Diffstat (limited to 'app/assets/javascripts/environments/components')
8 files changed, 470 insertions, 60 deletions
diff --git a/app/assets/javascripts/environments/components/canary_ingress.vue b/app/assets/javascripts/environments/components/canary_ingress.vue new file mode 100644 index 00000000000..f8cdbb96bc2 --- /dev/null +++ b/app/assets/javascripts/environments/components/canary_ingress.vue @@ -0,0 +1,109 @@ +<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> diff --git a/app/assets/javascripts/environments/components/canary_update_modal.vue b/app/assets/javascripts/environments/components/canary_update_modal.vue new file mode 100644 index 00000000000..fc63d6272c8 --- /dev/null +++ b/app/assets/javascripts/environments/components/canary_update_modal.vue @@ -0,0 +1,133 @@ +<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> diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index e7697f14802..c6b34fecbb7 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -10,11 +10,6 @@ export default { GlLoadingIcon, }, props: { - canaryDeploymentFeatureId: { - type: String, - required: false, - default: null, - }, isLoading: { type: Boolean, required: true, @@ -46,11 +41,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -75,8 +65,6 @@ export default { <environment-table :environments="environments" :can-read-environment="canReadEnvironment" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :show-canary-deployment-callout="showCanaryDeploymentCallout" :user-callouts-path="userCalloutsPath" :lock-promotion-svg-path="lockPromotionSvgPath" :help-canary-deployments-path="helpCanaryDeploymentsPath" diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue new file mode 100644 index 00000000000..07cb968d8d3 --- /dev/null +++ b/app/assets/javascripts/environments/components/deploy_board.vue @@ -0,0 +1,216 @@ +<script> +/* eslint-disable @gitlab/vue-require-i18n-strings */ +/** + * Renders a deploy board. + * + * A deploy board is composed by: + * - Information area with percentage of completion. + * - Instances with status. + * - Button Actions. + * [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png) + */ +import { isEmpty } from 'lodash'; +import { + GlIcon, + GlLoadingIcon, + GlLink, + GlTooltip, + GlTooltipDirective, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import deployBoardSvg from '@gitlab/svgs/dist/illustrations/deploy-boards.svg'; +import instanceComponent from '~/vue_shared/components/deployment_instance.vue'; +import { n__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { STATUS_MAP, CANARY_STATUS } from '../constants'; +import CanaryIngress from './canary_ingress.vue'; + +export default { + components: { + instanceComponent, + CanaryIngress, + GlIcon, + GlLoadingIcon, + GlLink, + GlTooltip, + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml, + }, + mixins: [glFeatureFlagsMixin()], + props: { + deployBoardData: { + type: Object, + required: true, + }, + deployBoardsHelpPath: { + type: String, + required: false, + default: '', + }, + isLoading: { + type: Boolean, + required: true, + }, + isEmpty: { + type: Boolean, + required: true, + }, + logsPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + canRenderDeployBoard() { + return !this.isEmpty && !isEmpty(this.deployBoardData); + }, + canRenderEmptyState() { + return this.isEmpty; + }, + canRenderCanaryWeight() { + return ( + this.glFeatures.canaryIngressWeightControl && !isEmpty(this.deployBoardData.canary_ingress) + ); + }, + instanceCount() { + const { instances } = this.deployBoardData; + + return Array.isArray(instances) ? instances.length : 0; + }, + instanceIsCompletedCount() { + const completionPercentage = this.deployBoardData.completion / 100; + const completionCount = Math.floor(completionPercentage * this.instanceCount); + + return Number.isNaN(completionCount) ? 0 : completionCount; + }, + instanceIsCompletedText() { + const title = n__('instance completed', 'instances completed', this.instanceIsCompletedCount); + + return `${this.instanceIsCompletedCount} ${title}`; + }, + instanceTitle() { + return n__('Instance', 'Instances', this.instanceCount); + }, + 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, + }; + }, + }, + methods: { + changeCanaryWeight(weight) { + this.$emit('changeCanaryWeight', weight); + }, + }, +}; +</script> +<template> + <div class="js-deploy-board deploy-board"> + <gl-loading-icon v-if="isLoading" class="loading-icon" /> + <template v-else> + <div v-if="canRenderDeployBoard" class="deploy-board-information gl-p-5"> + <div class="deploy-board-information gl-w-full"> + <section class="deploy-board-status"> + <span v-gl-tooltip :title="instanceIsCompletedText"> + <span ref="percentage" class="gl-text-center text-plain gl-font-lg" + >{{ deployBoardData.completion }}%</span + > + <span class="text text-center text-secondary">{{ __('Complete') }}</span> + </span> + </section> + + <section class="deploy-board-instances"> + <div class="gl-font-base text-secondary"> + <span class="deploy-board-instances-text" + >{{ instanceTitle }} ({{ instanceCount }})</span + > + <span ref="legend-icon" data-testid="legend-tooltip-target"> + <gl-icon class="gl-text-blue-500 gl-ml-2" name="question" /> + </span> + <gl-tooltip :target="() => $refs['legend-icon']" boundary="#content-body"> + <div class="deploy-board-legend gl-display-flex gl-flex-direction-column"> + <div + v-for="status in statuses" + :key="status.text" + class="gl-display-flex gl-align-items-center" + > + <instance-component :status="status.class" :stable="status.stable" /> + <span class="legend-text gl-ml-3">{{ status.text }}</span> + </div> + </div> + </gl-tooltip> + </div> + + <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> + + <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"> + <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-danger btn-inverted" + data-method="post" + rel="nofollow" + >{{ __('Abort') }}</gl-link + > + </section> + </div> + </div> + + <div v-if="canRenderEmptyState" class="deploy-board-empty"> + <section v-safe-html="deployBoardSvg" class="deploy-board-empty-state-svg"></section> + + <section class="deploy-board-empty-state-text"> + <span class="deploy-board-empty-state-title d-flex">{{ + __('Kubernetes deployment not found') + }}</span> + <span> + To see deployment progress for your environments, make sure you are deploying to + <code>$KUBE_NAMESPACE</code> and annotating with + <code>app.gitlab.com/app=$CI_PROJECT_PATH_SLUG</code> + and + <code>app.gitlab.com/env=$CI_ENVIRONMENT_SLUG</code>. + </span> + </section> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 347828888dc..1724cc692bd 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -226,7 +226,7 @@ export default { { deep: true }, ); const combinedActions = (manualActions || []).concat(scheduledActions || []); - return combinedActions.map(action => ({ + return combinedActions.map((action) => ({ ...action, name: action.name, })); diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 32528e6c6ea..48edde82ce7 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -55,7 +55,7 @@ export default { retryUrl: this.retryUrl, isLastDeployment: this.isLastDeployment, }); - eventHub.$on('rollbackEnvironment', environment => { + eventHub.$on('rollbackEnvironment', (environment) => { if (environment.id === this.environment.id) { this.isLoading = true; } diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index b6a7cce36e9..6f68c6e864a 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -39,11 +39,6 @@ export default { type: String, required: true, }, - canaryDeploymentFeatureId: { - type: String, - required: false, - default: '', - }, canCreateEnvironment: { type: Boolean, required: true, @@ -75,11 +70,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -116,7 +106,7 @@ export default { this.service .getFolderContent(folder.folder_path) - .then(response => this.store.setfolderContent(folder, response.data.environments)) + .then((response) => this.store.setfolderContent(folder, response.data.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) .catch(() => { Flash(s__('Environments|An error occurred while fetching the environments.')); @@ -130,7 +120,7 @@ export default { // We need to verify if any folder is open to also update it const openFolders = this.store.getOpenFolders(); if (openFolders.length) { - openFolders.forEach(folder => this.fetchChildEnvironments(folder)); + openFolders.forEach((folder) => this.fetchChildEnvironments(folder)); } }, }, @@ -205,8 +195,6 @@ export default { :environments="state.environments" :pagination="state.paginationInformation" :can-read-environment="canReadEnvironment" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :show-canary-deployment-callout="showCanaryDeploymentCallout" :user-callouts-path="userCalloutsPath" :lock-promotion-svg-path="lockPromotionSvgPath" :help-canary-deployments-path="helpCanaryDeploymentsPath" diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index d13c7204285..bbb56ca6f26 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -6,16 +6,16 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { flow, reverse, sortBy } from 'lodash/fp'; import { s__ } from '~/locale'; import EnvironmentItem from './environment_item.vue'; +import DeployBoard from './deploy_board.vue'; +import CanaryUpdateModal from './canary_update_modal.vue'; export default { components: { EnvironmentItem, GlLoadingIcon, - DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'), - CanaryDeploymentCallout: () => - import('ee_component/environments/components/canary_deployment_callout.vue'), + DeployBoard, EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), - CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'), + CanaryUpdateModal, }, props: { environments: { @@ -33,11 +33,6 @@ export default { required: false, default: false, }, - canaryDeploymentFeatureId: { - type: String, - required: false, - default: '', - }, helpCanaryDeploymentsPath: { type: String, required: false, @@ -48,11 +43,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -67,7 +57,7 @@ export default { }, computed: { sortedEnvironments() { - return this.sortEnvironments(this.environments).map(env => + return this.sortEnvironments(this.environments).map((env) => this.shouldRenderFolderContent(env) ? { ...env, children: this.sortEnvironments(env.children) } : env, @@ -121,9 +111,6 @@ export default { shouldRenderFolderContent(env) { return env.isFolder && env.isOpen && env.children && env.children.length > 0; }, - shouldShowCanaryCallout(env) { - return env.showCanaryCallout && this.showCanaryDeploymentCallout; - }, shouldRenderAlert(env) { return env?.has_opened_alert; }, @@ -144,11 +131,11 @@ export default { * 5. Put folders first. */ return flow( - sortBy(env => (env.isFolder ? env.folderName : env.name)), + sortBy((env) => (env.isFolder ? env.folderName : env.name)), reverse, - sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000')), + sortBy((env) => (env.last_deployment ? env.last_deployment.created_at : '0000')), reverse, - sortBy(env => (env.isFolder ? -1 : 1)), + sortBy((env) => (env.isFolder ? -1 : 1)), )(environments); }, changeCanaryWeight(model, weight) { @@ -241,17 +228,6 @@ export default { </div> </template> </template> - - <template v-if="shouldShowCanaryCallout(model)"> - <canary-deployment-callout - :key="`canary-promo-${i}`" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :user-callouts-path="userCalloutsPath" - :lock-promotion-svg-path="lockPromotionSvgPath" - :help-canary-deployments-path="helpCanaryDeploymentsPath" - :data-js-canary-promo-key="i" - /> - </template> </template> </div> </template> |