diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-05 18:10:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-05 18:10:25 +0000 |
commit | f368b4968e55b32dcedfaefe7c31f7a9463454cf (patch) | |
tree | b3e4652bd0131adf46f4b7e07346a0dbfa32da05 /app/assets/javascripts/environments | |
parent | 2c2b5aeac04350b0d3e13d4b52add0b520bf2ebb (diff) | |
download | gitlab-ce-f368b4968e55b32dcedfaefe7c31f7a9463454cf.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/environments')
10 files changed, 620 insertions, 11 deletions
diff --git a/app/assets/javascripts/environments/components/canary_deployment_callout.vue b/app/assets/javascripts/environments/components/canary_deployment_callout.vue new file mode 100644 index 00000000000..a5c0d78524b --- /dev/null +++ b/app/assets/javascripts/environments/components/canary_deployment_callout.vue @@ -0,0 +1,68 @@ +<script> +import { GlButton, GlLink, GlIcon } from '@gitlab/ui'; +import PersistentUserCallout from '~/persistent_user_callout'; + +export default { + components: { + GlButton, + GlLink, + GlIcon, + }, + props: { + canaryDeploymentFeatureId: { + type: String, + required: true, + }, + userCalloutsPath: { + type: String, + required: true, + }, + lockPromotionSvgPath: { + type: String, + required: true, + }, + helpCanaryDeploymentsPath: { + type: String, + required: true, + }, + }, + mounted() { + const callout = this.$refs['canary-deployment-callout']; + PersistentUserCallout.factory(callout); + }, +}; +</script> + +<template> + <div + ref="canary-deployment-callout" + class="p-3 canary-deployment-callout" + :data-dismiss-endpoint="userCalloutsPath" + :data-feature-id="canaryDeploymentFeatureId" + > + <img class="canary-deployment-callout-lock pr-3" :src="lockPromotionSvgPath" /> + + <div class="pl-3"> + <p class="font-weight-bold mb-1"> + {{ __('Upgrade plan to unlock Canary Deployments feature') }} + </p> + + <p class="canary-deployment-callout-message"> + {{ + __( + 'Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application.', + ) + }} + <gl-link :href="helpCanaryDeploymentsPath">{{ __('Read more') }}</gl-link> + </p> + + <gl-button href="https://about.gitlab.com/sales/" category="secondary" variant="info">{{ + __('Contact sales to upgrade') + }}</gl-button> + </div> + + <div class="ml-auto pr-2 canary-deployment-callout-close js-close"> + <gl-icon name="close" /> + </div> + </div> +</template> 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/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue new file mode 100644 index 00000000000..28e812d5220 --- /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 'empty_states/icons/_deploy_board.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/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 1efea30a5f7..ff183e51395 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -6,16 +6,18 @@ 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'; +import CanaryDeploymentCallout from './canary_deployment_callout.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, + CanaryDeploymentCallout, EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), - CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'), + CanaryUpdateModal, }, props: { environments: { diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js new file mode 100644 index 00000000000..6d427bef4e6 --- /dev/null +++ b/app/assets/javascripts/environments/constants.js @@ -0,0 +1,40 @@ +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, +}; + +export const CANARY_UPDATE_MODAL = 'confirm-canary-change'; diff --git a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql new file mode 100644 index 00000000000..04ea5cbcaef --- /dev/null +++ b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql @@ -0,0 +1,5 @@ +mutation($input: EnvironmentsCanaryIngressUpdateInput!) { + environmentsCanaryIngressUpdate(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 3d301f6094d..15a00c11ee6 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -3,7 +3,7 @@ */ import { isEqual, isFunction, omitBy } from 'lodash'; import Visibility from 'visibilityjs'; -import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store'; +import EnvironmentsStore from '../stores/environments_store'; import Poll from '../../lib/utils/poll'; import { getParameterByName } from '../../lib/utils/common_utils'; import { s__ } from '../../locale'; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 6ef8b9f643f..2f4f53953f6 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,4 +1,4 @@ -import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers'; +import { setDeployBoard } from './helpers'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; /** @@ -81,6 +81,17 @@ export default class EnvironmentsStore { this.state.environments = filteredEnvironments; + /** + * Add the canary callout banner underneath the second environment listed. + * + * If there is only one environment, then add to it underneath the first. + */ + if (this.state.environments.length >= 2) { + this.state.environments[1].showCanaryCallout = true; + } else if (this.state.environments.length === 1) { + this.state.environments[0].showCanaryCallout = true; + } + return filteredEnvironments; } @@ -135,12 +146,22 @@ export default class EnvironmentsStore { /** * Toggles deploy board visibility for the provided environment ID. - * Currently only works on EE. * * @param {Object} environment * @return {Array} */ - toggleDeployBoard() { + toggleDeployBoard(environmentID) { + const environments = this.state.environments.slice(); + + this.state.environments = environments.map((env) => { + let updated = { ...env }; + + if (env.id === environmentID) { + updated = { ...updated, isDeployBoardVisible: !env.isDeployBoardVisible }; + } + return updated; + }); + return this.state.environments; } diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js index eb47ba29412..89457da0614 100644 --- a/app/assets/javascripts/environments/stores/helpers.js +++ b/app/assets/javascripts/environments/stores/helpers.js @@ -1,7 +1,22 @@ /** - * Deploy boards are EE only. - * * @param {Object} environment * @returns {Object} */ -export const setDeployBoard = (oldEnvironmentState, environment) => environment; +export const setDeployBoard = (oldEnvironmentState, environment) => { + let parsedEnvironment = environment; + if (environment.size === 1 && environment.rollout_status) { + parsedEnvironment = { + ...environment, + hasDeployBoard: true, + isDeployBoardVisible: + oldEnvironmentState.isDeployBoardVisible === false + ? oldEnvironmentState.isDeployBoardVisible + : true, + deployBoardData: + environment.rollout_status.status === 'found' ? environment.rollout_status : {}, + isLoadingDeployBoard: environment.rollout_status.status === 'loading', + isEmptyDeployBoard: environment.rollout_status.status === 'not_found', + }; + } + return parsedEnvironment; +}; |