Commit b941f841 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo Committed by Paul Slaughter

Add deploy and re-deploy buttons to deployments

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25427
parent 7113fbd7
...@@ -6,3 +6,8 @@ export const RUNNING = 'running'; ...@@ -6,3 +6,8 @@ export const RUNNING = 'running';
export const SUCCESS = 'success'; export const SUCCESS = 'success';
export const FAILED = 'failed'; export const FAILED = 'failed';
export const CANCELED = 'canceled'; export const CANCELED = 'canceled';
// ACTION STATUSES
export const STOPPING = 'stopping';
export const DEPLOYING = 'deploying';
export const REDEPLOYING = 'redeploying';
<script> <script>
import { __, s__ } from '~/locale'; import DeploymentActions from './deployment_actions.vue';
import DeploymentInfo from './deployment_info.vue'; import DeploymentInfo from './deployment_info.vue';
import DeploymentViewButton from './deployment_view_button.vue'; import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED } from './constants';
import DeploymentStopButton from './deployment_stop_button.vue';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
export default { export default {
// name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives // name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Deployment', name: 'Deployment',
components: { components: {
DeploymentActions,
DeploymentInfo, DeploymentInfo,
DeploymentStopButton,
DeploymentViewButton,
}, },
props: { props: {
deployment: { deployment: {
...@@ -40,38 +37,14 @@ export default { ...@@ -40,38 +37,14 @@ export default {
}, },
}, },
computed: { computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY;
},
computedDeploymentStatus() { computedDeploymentStatus() {
if (this.deployment.status === CREATED) { if (this.deployment.status === CREATED) {
return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY; return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
} }
return this.deployment.status; return this.deployment.status;
}, },
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
isManual() { isManual() {
return Boolean( return Boolean(this.deployment.details?.playable_build?.play_path);
this.deployment.details &&
this.deployment.details.playable_build &&
this.deployment.details.playable_build.play_path,
);
},
isDeployInProgress() {
return this.deployment.status === RUNNING;
}, },
}, },
}; };
...@@ -87,22 +60,12 @@ export default { ...@@ -87,22 +60,12 @@ export default {
:deployment="deployment" :deployment="deployment"
:show-metrics="showMetrics" :show-metrics="showMetrics"
/> />
<div> <deployment-actions
<!-- show appropriate version of review app button -->
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment" :deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp" :show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta" :visual-review-app-metadata="visualReviewAppMeta"
/> />
<!-- if it is stoppable, show stop -->
<deployment-stop-button
v-if="deployment.stop_url"
:is-deploy-in-progress="isDeployInProgress"
:stop-url="deployment.stop_url"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
......
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
export default {
name: 'DeploymentActionButton',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
actionsConfiguration: {
type: Object,
required: true,
},
actionInProgress: {
type: String,
required: false,
default: null,
},
buttonTitle: {
type: String,
required: false,
default: '',
},
computedDeploymentStatus: {
type: String,
required: true,
},
containerClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
isActionInProgress() {
return Boolean(this.computedDeploymentStatus === RUNNING || this.actionInProgress);
},
actionInProgressTooltip() {
switch (this.actionInProgress) {
case this.actionsConfiguration.actionName:
return this.actionsConfiguration.busyText;
case null:
return '';
default:
return __('Another action is currently in progress');
}
},
isLoading() {
return this.actionInProgress === this.actionsConfiguration.actionName;
},
},
};
</script>
<template>
<span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0">
<gl-button
v-gl-tooltip
:title="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`btn btn-default btn-sm inline prepend-left-4 ${containerClasses}`"
@click="$emit('click')"
>
<span class="d-inline-flex align-items-baseline">
<slot> </slot>
</span>
</gl-button>
</span>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
import DeploymentActionButton from './deployment_action_button.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants';
export default {
name: 'DeploymentActions',
components: {
DeploymentActionButton,
DeploymentViewButton,
GlIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
computedDeploymentStatus: {
type: String,
required: true,
},
deployment: {
type: Object,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
data() {
return {
actionInProgress: null,
constants: {
STOPPING,
DEPLOYING,
REDEPLOYING,
},
};
},
computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY && Boolean(this.playPath);
},
canBeManuallyRedeployed() {
return this.computedDeploymentStatus === FAILED && Boolean(this.redeployPath);
},
shouldShowManualButtons() {
return this.glFeatures.deployFromFooter;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
playPath() {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
return this.deployment.details?.playable_build?.retry_path;
},
stopUrl() {
return this.deployment.stop_url;
},
},
actionsConfiguration: {
[STOPPING]: {
actionName: STOPPING,
buttonText: s__('MrDeploymentActions|Stop environment'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to stop this environment?'),
errorMessage: __('Something went wrong while stopping this environment. Please try again.'),
},
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: s__('MrDeploymentActions|Deploy'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: s__('MrDeploymentActions|Re-deploy'),
busyText: __('This environment is being re-deployed'),
confirmMessage: __('Are you sure you want to re-deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
},
methods: {
executeAction(endpoint, { actionName, confirmMessage, errorMessage }) {
const isConfirmed = confirm(confirmMessage); //eslint-disable-line
if (isConfirmed) {
this.actionInProgress = actionName;
MRWidgetService.executeInlineAction(endpoint)
.then(resp => {
const redirectUrl = resp?.data?.redirect_url;
if (redirectUrl) {
visitUrl(redirectUrl);
}
})
.catch(() => {
createFlash(errorMessage);
})
.finally(() => {
this.actionInProgress = null;
});
}
},
stopEnvironment() {
this.executeAction(this.stopUrl, this.$options.actionsConfiguration[STOPPING]);
},
deployManually() {
this.executeAction(this.playPath, this.$options.actionsConfiguration[DEPLOYING]);
},
redeploy() {
this.executeAction(this.redeployPath, this.$options.actionsConfiguration[REDEPLOYING]);
},
},
};
</script>
<template>
<div>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyDeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-deploy-action"
@click="deployManually"
>
<gl-icon name="play" />
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyRedeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-redeploy-action"
@click="redeploy"
>
<gl-icon name="repeat" />
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
:action-in-progress="actionInProgress"
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
:button-title="$options.actionsConfiguration[constants.STOPPING].buttonText"
container-classes="js-stop-env"
@click="stopEnvironment"
>
<gl-icon name="stop" />
</deployment-action-button>
</div>
</template>
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'DeploymentStopButton',
components: {
LoadingButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
isDeployInProgress: {
type: Boolean,
required: true,
},
stopUrl: {
type: String,
required: true,
},
},
data() {
return {
isStopping: false,
};
},
computed: {
deployInProgressTooltip() {
return this.isDeployInProgress
? __('Stopping this environment is currently not possible as a deployment is in progress')
: '';
},
},
methods: {
stopEnvironment() {
const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
this.isStopping = true;
MRWidgetService.stopEnvironment(this.stopUrl)
.then(res => res.data)
.then(data => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
this.isStopping = false;
})
.catch(() => {
createFlash(
__('Something went wrong while stopping this environment. Please try again.'),
);
this.isStopping = false;
});
}
},
},
};
</script>
<template>
<span v-gl-tooltip :title="deployInProgressTooltip" class="d-inline-block" tabindex="0">
<loading-button
v-gl-tooltip
:loading="isStopping"
:disabled="isDeployInProgress"
:title="__('Stop environment')"
container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
@click="stopEnvironment"
>
<icon name="stop" />
</loading-button>
</span>
</template>
...@@ -54,7 +54,7 @@ export default class MRWidgetService { ...@@ -54,7 +54,7 @@ export default class MRWidgetService {
return axios.post(this.endpoints.rebasePath); return axios.post(this.endpoints.rebasePath);
} }
static stopEnvironment(url) { static executeInlineAction(url) {
return axios.post(url); return axios.post(url);
} }
......
...@@ -20,6 +20,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -20,6 +20,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true) push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true)
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:single_mr_diff_view, @project) push_frontend_feature_flag(:single_mr_diff_view, @project)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end end
......
---
title: Add deploy and re-deploy buttons to deployments
merge_request: 25427
author:
type: added
...@@ -2038,6 +2038,9 @@ msgstr "" ...@@ -2038,6 +2038,9 @@ msgstr ""
msgid "Anonymous" msgid "Anonymous"
msgstr "" msgstr ""
msgid "Another action is currently in progress"
msgstr ""
msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time" msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time"
msgstr "" msgstr ""
...@@ -2283,6 +2286,9 @@ msgstr "" ...@@ -2283,6 +2286,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone." msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr "" msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
msgid "Are you sure you want to erase this build?" msgid "Are you sure you want to erase this build?"
msgstr "" msgstr ""
...@@ -2298,6 +2304,9 @@ msgstr "" ...@@ -2298,6 +2304,9 @@ msgstr ""
msgid "Are you sure you want to permanently delete this license?" msgid "Are you sure you want to permanently delete this license?"
msgstr "" msgstr ""
msgid "Are you sure you want to re-deploy this environment?"
msgstr ""
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again." msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr "" msgstr ""
...@@ -12799,6 +12808,15 @@ msgstr "" ...@@ -12799,6 +12808,15 @@ msgstr ""
msgid "Moves this issue to %{path_to_project}." msgid "Moves this issue to %{path_to_project}."
msgstr "" msgstr ""
msgid "MrDeploymentActions|Deploy"
msgstr ""
msgid "MrDeploymentActions|Re-deploy"
msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multiple issue boards" msgid "Multiple issue boards"
msgstr "" msgstr ""
...@@ -18243,6 +18261,9 @@ msgstr "" ...@@ -18243,6 +18261,9 @@ msgstr ""
msgid "Something went wrong while deleting your note. Please try again." msgid "Something went wrong while deleting your note. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while deploying this environment. Please try again."
msgstr ""
msgid "Something went wrong while editing your comment. Please try again." msgid "Something went wrong while editing your comment. Please try again."
msgstr "" msgstr ""
...@@ -18741,9 +18762,6 @@ msgstr "" ...@@ -18741,9 +18762,6 @@ msgstr ""
msgid "Stop Terminal" msgid "Stop Terminal"
msgstr "" msgstr ""
msgid "Stop environment"
msgstr ""
msgid "Stop impersonation" msgid "Stop impersonation"
msgstr "" msgstr ""
...@@ -18753,9 +18771,6 @@ msgstr "" ...@@ -18753,9 +18771,6 @@ msgstr ""
msgid "Stopped" msgid "Stopped"
msgstr "" msgstr ""
msgid "Stopping this environment is currently not possible as a deployment is in progress"
msgstr ""
msgid "Stopping..." msgid "Stopping..."
msgstr "" msgstr ""
...@@ -20002,6 +20017,12 @@ msgstr "" ...@@ -20002,6 +20017,12 @@ msgstr ""
msgid "This environment has no deployments yet." msgid "This environment has no deployments yet."
msgstr "" msgstr ""
msgid "This environment is being deployed"
msgstr ""
msgid "This environment is being re-deployed"
msgstr ""
msgid "This epic already has the maximum number of child epics." msgid "This epic already has the maximum number of child epics."
msgstr "" msgstr ""
......
import { mount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlButton } from '@gitlab/ui';
import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
import {
CREATED,
RUNNING,
DEPLOYING,
REDEPLOYING,
} from '~/vue_merge_request_widget/components/deployment/constants';
import { actionButtonMocks } from './deployment_mock_data';
const baseProps = {
actionsConfiguration: actionButtonMocks[DEPLOYING],
actionInProgress: null,
computedDeploymentStatus: CREATED,
};
describe('Deployment action button', () => {
let wrapper;
const factory = (options = {}) => {
wrapper = mount(DeploymentActionButton, {
...options,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when passed only icon', () => {
beforeEach(() => {
factory({
propsData: baseProps,
slots: { default: ['<gl-icon name="stop" />'] },
stubs: {
'gl-icon': GlIcon,
},
});
});
it('renders slot correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
});
});
describe('when passed multiple items', () => {
beforeEach(() => {
factory({
propsData: baseProps,
slots: {
default: ['<gl-icon name="play" />', `<span>${actionButtonMocks[DEPLOYING]}</span>`],
},
stubs: {
'gl-icon': GlIcon,
},
});
});
it('renders slot correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
expect(wrapper.text()).toContain(actionButtonMocks[DEPLOYING]);
});
});
describe('when its action is in progress', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[DEPLOYING].actionName,
},
});
});
it('is disabled and shows the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when another action is in progress', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[REDEPLOYING].actionName,
},
});
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when action status is running', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[REDEPLOYING].actionName,
computedDeploymentStatus: RUNNING,
},
});
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
describe('when no action is in progress', () => {
beforeEach(() => {
factory({
propsData: baseProps,
});
});
it('is not disabled nor does it show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlButton).props('disabled')).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
import {
CREATED,
MANUAL_DEPLOY,
FAILED,
DEPLOYING,
REDEPLOYING,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
import {
actionButtonMocks,
deploymentMockData,
playDetails,
retryDetails,
} from './deployment_mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('DeploymentAction component', () => {
let wrapper;
let executeActionSpy;
const factory = (options = {}) => {
// This destroys any wrappers created before a nested call to factory reassigns it
if (wrapper && wrapper.destroy) {
wrapper.destroy();
}
wrapper = mount(DeploymentActions, {
...options,
provide: { glFeatures: { deployFromFooter: true } },
});
};
const findStopButton = () => wrapper.find('.js-stop-env');
const findDeployButton = () => wrapper.find('.js-manual-deploy-action');
const findRedeployButton = () => wrapper.find('.js-manual-redeploy-action');
beforeEach(() => {
executeActionSpy = jest.spyOn(MRWidgetService, 'executeInlineAction');
factory({
propsData: {
computedDeploymentStatus: CREATED,
deployment: deploymentMockData,
showVisualReviewApp: false,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('actions do not appear when conditions are unmet', () => {
describe('when there is no stop_url', () => {
beforeEach(() => {
factory({
propsData: {
computedDeploymentStatus: CREATED,
deployment: {
...deploymentMockData,
stop_url: null,
},
showVisualReviewApp: false,
},
});
});
it('the stop button does not appear', () => {
expect(findStopButton().exists()).toBe(false);
});
});
describe('when there is no play_path in details', () => {
it('the manual deploy button does not appear', () => {
expect(findDeployButton().exists()).toBe(false);
});
});
describe('when there is no retry_path in details', () => {
it('the manual redeploy button does not appear', () => {
expect(findRedeployButton().exists()).toBe(false);
});
});
});
describe('when conditions are met', () => {
describe.each`
configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint
${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url}
${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path}
${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path}
`(
'$configConst action',
({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
describe(`${configConst} action`, () => {
const confirmAction = () => {
jest.spyOn(window, 'confirm').mockReturnValueOnce(true);
finderFn().trigger('click');
};
const rejectAction = () => {
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
finderFn().trigger('click');
};
beforeEach(() => {
factory({
propsData: {
computedDeploymentStatus,
deployment: {
...deploymentMockData,
details: displayConditionChanges,
},
showVisualReviewApp: false,
},
});
});
it('the button is rendered', () => {
expect(finderFn().exists()).toBe(true);
});
describe('when clicked', () => {
describe('should show a confirm dialog but not call executeInlineAction when declined', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
rejectAction();
});
it('should show the confirm dialog', () => {
expect(window.confirm).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
);
});
it('should not execute the action', () => {
expect(MRWidgetService.executeInlineAction).not.toHaveBeenCalled();
});
});
describe('should show a confirm dialog and call executeInlineAction when accepted', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
confirmAction();
});
it('should show the confirm dialog', () => {
expect(window.confirm).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalledWith(
actionButtonMocks[configConst].confirmMessage,
);
});
it('should execute the action with expected URL', () => {
expect(MRWidgetService.executeInlineAction).toHaveBeenCalled();
expect(MRWidgetService.executeInlineAction).toHaveBeenCalledWith(endpoint);
});
it('should not throw an error', () => {
expect(createFlash).not.toHaveBeenCalled();
});
describe('response includes redirect_url', () => {
const url = '/root/example';
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce({
data: { redirect_url: url },
});
confirmAction();
});
it('calls visit url with the redirect_url', () => {
expect(visitUrl).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(url);
});
});
describe('it should call the executeAction method ', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
confirmAction();
});
it('calls with the expected arguments', () => {
expect(wrapper.vm.executeAction).toHaveBeenCalled();
expect(wrapper.vm.executeAction).toHaveBeenCalledWith(
endpoint,
actionButtonMocks[configConst],
);
});
});
describe('when executeInlineAction errors', () => {
beforeEach(() => {
executeActionSpy.mockRejectedValueOnce();
confirmAction();
});
it('should call createFlash with error message', () => {
expect(createFlash).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith(
actionButtonMocks[configConst].errorMessage,
);
});
});
});
});
});
},
);
});
});
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; import {
DEPLOYING,
REDEPLOYING,
SUCCESS,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
const actionButtonMocks = {
[STOPPING]: {
actionName: STOPPING,
buttonText: 'Stop environment',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to stop this environment?',
errorMessage: 'Something went wrong while stopping this environment. Please try again.',
},
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: 'Deploy',
busyText: 'This environment is being deployed',
confirmMessage: 'Are you sure you want to deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
},
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: 'Re-deploy',
busyText: 'This environment is being re-deployed',
confirmMessage: 'Are you sure you want to re-deploy this environment?',
errorMessage: 'Something went wrong while deploying this environment. Please try again.',
},
};
const deploymentMockData = { const deploymentMockData = {
id: 15, id: 15,
...@@ -29,4 +58,16 @@ const deploymentMockData = { ...@@ -29,4 +58,16 @@ const deploymentMockData = {
], ],
}; };
export default deploymentMockData; const playDetails = {
playable_build: {
play_path: '/root/test-deployments/-/jobs/1131/play',
},
};
const retryDetails = {
playable_build: {
retry_path: '/root/test-deployments/-/jobs/1131/retry',
},
};
export { actionButtonMocks, deploymentMockData, playDetails, retryDetails };
...@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue'; import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue'; import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
import { import {
CREATED, CREATED,
RUNNING, RUNNING,
...@@ -10,15 +9,7 @@ import { ...@@ -10,15 +9,7 @@ import {
FAILED, FAILED,
CANCELED, CANCELED,
} from '~/vue_merge_request_widget/components/deployment/constants'; } from '~/vue_merge_request_widget/components/deployment/constants';
import deploymentMockData from './deployment_mock_data'; import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data';
const deployDetail = {
playable_build: {
retry_path: '/root/test-deployments/-/jobs/1131/retry',
play_path: '/root/test-deployments/-/jobs/1131/play',
},
isManual: true,
};
describe('Deployment component', () => { describe('Deployment component', () => {
let wrapper; let wrapper;
...@@ -30,6 +21,7 @@ describe('Deployment component', () => { ...@@ -30,6 +21,7 @@ describe('Deployment component', () => {
} }
wrapper = mount(DeploymentComponent, { wrapper = mount(DeploymentComponent, {
...options, ...options,
provide: { glFeatures: { deployFromFooter: true } },
}); });
}; };
...@@ -53,28 +45,39 @@ describe('Deployment component', () => { ...@@ -53,28 +45,39 @@ describe('Deployment component', () => {
describe('status message and buttons', () => { describe('status message and buttons', () => {
const noActions = []; const noActions = [];
const noDetails = { isManual: false }; const noDetails = { isManual: false };
const deployGroup = [DeploymentViewButton, DeploymentStopButton]; const deployDetail = {
...playDetails,
isManual: true,
};
const retryDetail = {
...retryDetails,
isManual: true,
};
const defaultGroup = ['.js-deploy-url', '.js-stop-env'];
const manualDeployGroup = ['.js-manual-deploy-action', ...defaultGroup];
const manualRedeployGroup = ['.js-manual-redeploy-action', ...defaultGroup];
describe.each` describe.each`
status | previous | deploymentDetails | text | actionButtons status | previous | deploymentDetails | text | actionButtons
${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${deployGroup} ${CREATED} | ${true} | ${deployDetail} | ${'Can be manually deployed to'} | ${manualDeployGroup}
${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup} ${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${defaultGroup}
${CREATED} | ${false} | ${deployDetail} | ${'Can be manually deployed to'} | ${noActions} ${CREATED} | ${false} | ${deployDetail} | ${'Can be manually deployed to'} | ${noActions}
${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions} ${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions}
${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup} ${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${defaultGroup}
${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup} ${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${defaultGroup}
${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions} ${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions}
${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions} ${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions}
${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} ${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup} ${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} ${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup} ${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup} ${FAILED} | ${true} | ${retryDetail} | ${'Failed to deploy to'} | ${manualRedeployGroup}
${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup} ${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${defaultGroup}
${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions} ${FAILED} | ${false} | ${retryDetail} | ${'Failed to deploy to'} | ${noActions}
${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions} ${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions}
${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${deployGroup} ${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deployment to'} | ${defaultGroup}
${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${deployGroup} ${CANCELED} | ${true} | ${noDetails} | ${'Canceled deployment to'} | ${defaultGroup}
${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deployment to'} | ${noActions} ${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deployment to'} | ${noActions}
${CANCELED} | ${false} | ${noDetails} | ${'Canceled deployment to'} | ${noActions} ${CANCELED} | ${false} | ${noDetails} | ${'Canceled deployment to'} | ${noActions}
`( `(
...@@ -112,7 +115,7 @@ describe('Deployment component', () => { ...@@ -112,7 +115,7 @@ describe('Deployment component', () => {
if (actionButtons.length > 0) { if (actionButtons.length > 0) {
describe('renders the expected button group', () => { describe('renders the expected button group', () => {
actionButtons.forEach(button => { actionButtons.forEach(button => {
it(`renders ${button.name}`, () => { it(`renders ${button}`, () => {
expect(wrapper.find(button).exists()).toBe(true); expect(wrapper.find(button).exists()).toBe(true);
}); });
}); });
...@@ -121,8 +124,8 @@ describe('Deployment component', () => { ...@@ -121,8 +124,8 @@ describe('Deployment component', () => {
if (actionButtons.length === 0) { if (actionButtons.length === 0) {
describe('does not render the button group', () => { describe('does not render the button group', () => {
[DeploymentViewButton, DeploymentStopButton].forEach(button => { defaultGroup.forEach(button => {
it(`does not render ${button.name}`, () => { it(`does not render ${button}`, () => {
expect(wrapper.find(button).exists()).toBe(false); expect(wrapper.find(button).exists()).toBe(false);
}); });
}); });
...@@ -144,10 +147,6 @@ describe('Deployment component', () => { ...@@ -144,10 +147,6 @@ describe('Deployment component', () => {
describe('hasExternalUrls', () => { describe('hasExternalUrls', () => {
describe('when deployment has both external_url_formatted and external_url', () => { describe('when deployment has both external_url_formatted and external_url', () => {
it('should return true', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(true);
});
it('should render the View Button', () => { it('should render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(true); expect(wrapper.find(DeploymentViewButton).exists()).toBe(true);
}); });
...@@ -163,10 +162,6 @@ describe('Deployment component', () => { ...@@ -163,10 +162,6 @@ describe('Deployment component', () => {
}); });
}); });
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => { it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
}); });
...@@ -182,10 +177,6 @@ describe('Deployment component', () => { ...@@ -182,10 +177,6 @@ describe('Deployment component', () => {
}); });
}); });
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => { it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
}); });
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue'; import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
import deploymentMockData from './deployment_mock_data'; import { deploymentMockData } from './deployment_mock_data';
const appButtonText = { const appButtonText = {
text: 'View app', text: 'View app',
......
import Vue from 'vue';
import deploymentStopComponent from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Deployment component', () => {
const Component = Vue.extend(deploymentStopComponent);
let deploymentMockData;
beforeEach(() => {
deploymentMockData = {
id: 15,
name: 'review/diplo',
url: '/root/review-apps/environments/15',
stop_url: '/root/review-apps/environments/15/stop',
metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
external_url: 'http://gitlab.com.',
external_url_formatted: 'gitlab',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
deployment_manual_actions: [],
status: SUCCESS,
changes: [
{
path: 'index.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
},
{
path: 'imgs/gallery.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
},
{
path: 'about/',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
},
],
};
});
let vm;
afterEach(() => {
vm.$destroy();
});
describe('', () => {
beforeEach(() => {
vm = mountComponent(Component, {
stopUrl: deploymentMockData.stop_url,
isDeployInProgress: false,
});
});
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () =>
new Promise(resolve => {
resolve({
data: {
redirect_url: url,
},
});
});
const mockStopEnvironment = () => {
vm.stopEnvironment(deploymentMockData);
return vm;
};
it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
const visitUrl = spyOnDependency(deploymentStopComponent, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
expect(visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
it('should show a confirm dialog but should not work if the dialog is rejected', () => {
spyOn(window, 'confirm').and.returnValue(false);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
});
});
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