Commit 6e191adb authored by Phil Hughes's avatar Phil Hughes

Merge branch '25351-texts-and-structure' into 'master'

Update information and button text for deployment footer

See merge request gitlab-org/gitlab!18918
parents 3511db37 cdbe1fbd
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import { __ } from '~/locale';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import createFlash from '../../flash';
import MemoryUsage from './memory_usage.vue';
import StatusIcon from './mr_widget_status_icon.vue';
import ReviewAppLink from './review_app_link.vue';
import MRWidgetService from '../services/mr_widget_service';
export default {
// 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
name: 'Deployment',
components: {
LoadingButton,
MemoryUsage,
StatusIcon,
Icon,
TooltipOnTruncate,
FilteredSearchDropdown,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
deployment: {
type: Object,
required: true,
},
showMetrics: {
type: Boolean,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
deployedTextMap: {
running: __('Deploying to'),
success: __('Deployed to'),
failed: __('Failed to deploy to'),
created: __('Will deploy to'),
canceled: __('Failed to deploy to'),
},
data() {
return {
isStopping: false,
};
},
computed: {
deployTimeago() {
return this.timeFormated(this.deployment.deployed_at);
},
deploymentExternalUrl() {
if (this.deployment.changes && this.deployment.changes.length === 1) {
return this.deployment.changes[0].external_url;
}
return this.deployment.external_url;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
hasDeploymentTime() {
return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
},
hasDeploymentMeta() {
return Boolean(this.deployment.url && this.deployment.name);
},
hasMetrics() {
return Boolean(this.deployment.metrics_url);
},
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
},
isDeployInProgress() {
return this.deployment.status === 'running';
},
deployInProgressTooltip() {
return this.isDeployInProgress
? __('Stopping this environment is currently not possible as a deployment is in progress')
: '';
},
shouldRenderDropdown() {
return this.deployment.changes && this.deployment.changes.length > 1;
},
showMemoryUsage() {
return this.hasMetrics && this.showMetrics;
},
},
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.deployment.stop_url)
.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>
<div class="deploy-heading">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
<div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span> {{ deployedText }} </span>
<tooltip-on-truncate
:title="deployment.name"
truncate-target="child"
class="deploy-link label-truncate"
>
<a
:href="deployment.url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-meta"
>
{{ deployment.name }}
</a>
</tooltip-on-truncate>
</template>
<span
v-if="hasDeploymentTime"
v-gl-tooltip
:title="deployment.deployed_at_formatted"
class="js-deploy-time"
>
{{ deployTimeago }}
</span>
<memory-usage
v-if="showMemoryUsage"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
<div>
<template v-if="hasExternalUrls">
<filtered-search-dropdown
v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
:main-action-link="deploymentExternalUrl"
filter-key="path"
>
<template slot="mainAction" slot-scope="slotProps">
<review-app-link
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
</template>
<template slot="result" slot-scope="slotProps">
<a
:href="slotProps.result.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="menu-item"
>
<strong class="str-truncated-100 append-bottom-0 d-block">
{{ slotProps.result.path }}
</strong>
<p class="text-secondary str-truncated-100 append-bottom-0 d-block">
{{ slotProps.result.external_url }}
</p>
</a>
</template>
</filtered-search-dropdown>
<template v-else>
<review-app-link
:link="deploymentExternalUrl"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
</template>
<visual-review-app-link
v-if="showVisualReviewApp"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
/>
</template>
<span
v-if="deployment.stop_url"
v-gl-tooltip
:title="deployInProgressTooltip"
class="d-inline-block"
tabindex="0"
>
<loading-button
: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>
</div>
</div>
</div>
</div>
</div>
</template>
// DEPLOYMENT STATUSES
export const CREATED = 'created';
export const MANUAL_DEPLOY = 'manual_deploy';
export const WILL_DEPLOY = 'will_deploy';
export const RUNNING = 'running';
export const SUCCESS = 'success';
export const FAILED = 'failed';
export const CANCELED = 'canceled';
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import DeploymentInfo from './deployment_info.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import DeploymentStopButton from './deployment_stop_button.vue';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
export default {
// 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
name: 'Deployment',
components: {
DeploymentInfo,
DeploymentStopButton,
DeploymentViewButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
deployment: {
type: Object,
required: true,
},
showMetrics: {
type: Boolean,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
computed: {
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY;
},
computedDeploymentStatus() {
if (this.deployment.status === CREATED) {
return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
}
return this.deployment.status;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
hasPreviousDeployment() {
return Boolean(!this.isCurrent && this.deployment.deployed_at);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
isManual() {
return Boolean(
this.deployment.details &&
this.deployment.details.playable_build &&
this.deployment.details.playable_build.play_path,
);
},
isDeployInProgress() {
return this.deployment.status === RUNNING;
},
},
};
</script>
<template>
<div class="deploy-heading">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
<deployment-info
:computed-deployment-status="computedDeploymentStatus"
:deployment="deployment"
:show-metrics="showMetrics"
/>
<div>
<!-- show appropriate version of review app button -->
<deployment-view-button
v-if="hasExternalUrls"
:is-current="isCurrent"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
: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>
</template>
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import MemoryUsage from './memory_usage.vue';
import { MANUAL_DEPLOY, WILL_DEPLOY, RUNNING, SUCCESS, FAILED, CANCELED } from './constants';
export default {
name: 'DeploymentInfo',
components: {
GlLink,
MemoryUsage,
TooltipOnTruncate,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
computedDeploymentStatus: {
type: String,
required: true,
},
deployment: {
type: Object,
required: true,
},
showMetrics: {
type: Boolean,
required: true,
},
},
deployedTextMap: {
[MANUAL_DEPLOY]: __('Can deploy manually to'),
[WILL_DEPLOY]: __('Will deploy to'),
[RUNNING]: __('Deploying to'),
[SUCCESS]: __('Deployed to'),
[FAILED]: __('Failed to deploy to'),
[CANCELED]: __('Canceled deploy to'),
},
computed: {
deployTimeago() {
return this.timeFormated(this.deployment.deployed_at);
},
deployedText() {
return this.$options.deployedTextMap[this.computedDeploymentStatus];
},
hasDeploymentTime() {
return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
},
hasDeploymentMeta() {
return Boolean(this.deployment.url && this.deployment.name);
},
hasMetrics() {
return Boolean(this.deployment.metrics_url);
},
showMemoryUsage() {
return this.hasMetrics && this.showMetrics;
},
},
};
</script>
<template>
<div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span>{{ deployedText }}</span>
<tooltip-on-truncate
:title="deployment.name"
truncate-target="child"
class="deploy-link label-truncate"
>
<gl-link
:href="deployment.url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-meta gl-font-size-12"
>
{{ deployment.name }}
</gl-link>
</tooltip-on-truncate>
</template>
<span
v-if="hasDeploymentTime"
v-gl-tooltip
:title="deployment.deployed_at_formatted"
class="js-deploy-time"
>
{{ deployTimeago }}
</span>
<memory-usage
v-if="showMemoryUsage"
:metrics-url="deployment.metrics_url"
:metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
</template>
<script>
import { __ } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
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>
<script>
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import ReviewAppLink from '../review_app_link.vue';
export default {
name: 'DeploymentViewButton',
components: {
FilteredSearchDropdown,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
props: {
deployment: {
type: Object,
required: true,
},
isCurrent: {
type: Boolean,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
computed: {
deploymentExternalUrl() {
if (this.deployment.changes && this.deployment.changes.length === 1) {
return this.deployment.changes[0].external_url;
}
return this.deployment.external_url;
},
shouldRenderDropdown() {
return this.deployment.changes && this.deployment.changes.length > 1;
},
},
};
</script>
<template>
<span>
<filtered-search-dropdown
v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
:main-action-link="deploymentExternalUrl"
filter-key="path"
>
<template slot="mainAction" slot-scope="slotProps">
<review-app-link
:is-current="isCurrent"
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
</template>
<template slot="result" slot-scope="slotProps">
<a
:href="slotProps.result.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url-menu-item menu-item"
>
<strong class="str-truncated-100 append-bottom-0 d-block">
{{ slotProps.result.path }}
</strong>
<p class="text-secondary str-truncated-100 append-bottom-0 d-block">
{{ slotProps.result.external_url }}
</p>
</a>
</template>
</filtered-search-dropdown>
<template v-else>
<review-app-link
:is-current="isCurrent"
:link="deploymentExternalUrl"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
</template>
<visual-review-app-link
v-if="showVisualReviewApp"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
/>
</span>
</template>
<script>
import { sprintf, s__ } from '~/locale';
import statusCodes from '../../lib/utils/http_status';
import { bytesToMiB } from '../../lib/utils/number_utils';
import { backOff } from '../../lib/utils/common_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph.vue';
import MRWidgetService from '../services/mr_widget_service';
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { backOff } from '~/lib/utils/common_utils';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'MemoryUsage',
......
<script>
import _ from 'underscore';
import ArtifactsApp from './artifacts_list_app.vue';
import Deployment from './deployment.vue';
import Deployment from './deployment/deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......
<script>
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
......@@ -6,15 +7,24 @@ export default {
Icon,
},
props: {
link: {
cssClass: {
type: String,
required: true,
},
cssClass: {
isCurrent: {
type: Boolean,
required: true,
},
link: {
type: String,
required: true,
},
},
computed: {
linkText() {
return this.isCurrent ? __('View app') : __('View previous app');
},
},
};
</script>
<template>
......@@ -26,6 +36,6 @@ export default {
data-track-event="open_review_app"
data-track-label="review_app"
>
{{ __('View app') }} <icon class="fgray" name="external-link" />
{{ linkText }} <icon class="fgray" name="external-link" />
</a>
</template>
......@@ -10,7 +10,7 @@ import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment.vue';
import Deployment from './components/deployment/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
......
---
title: Update information and button text for deployment footer
merge_request: 18918
author:
type: changed
......@@ -73,6 +73,7 @@ export default {
<review-app-link
v-else-if="environment.external_url"
:link="environment.external_url"
:is-current="true"
css-class="btn btn-default btn-sm"
/>
</div>
......
......@@ -25,6 +25,7 @@ exports[`Environment Header has a failed pipeline matches the snapshot 1`] = `
<reviewapplink-stub
cssclass="btn btn-default btn-sm"
iscurrent="true"
link="http://example.com"
/>
</div>
......@@ -55,6 +56,7 @@ exports[`Environment Header has errors matches the snapshot 1`] = `
<reviewapplink-stub
cssclass="btn btn-default btn-sm"
iscurrent="true"
link="http://example.com"
/>
</div>
......@@ -85,6 +87,7 @@ exports[`Environment Header renders name and link to app matches the snapshot 1`
<reviewapplink-stub
cssclass="btn btn-default btn-sm"
iscurrent="true"
link="http://example.com"
/>
</div>
......
......@@ -6,6 +6,7 @@ import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import state from 'ee/vue_shared/security_reports/store/state';
import mockData, {
......@@ -861,6 +862,7 @@ describe('ee merge request widget options', () => {
external_url_formatted: 'diplo.',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
status: SUCCESS,
};
beforeEach(done => {
......
......@@ -2942,6 +2942,9 @@ msgstr ""
msgid "Callback URL"
msgstr ""
msgid "Can deploy manually to"
msgstr ""
msgid "Can override approvers and approvals required per merge request"
msgstr ""
......@@ -2969,6 +2972,9 @@ msgstr ""
msgid "Cancel this job"
msgstr ""
msgid "Canceled deploy to"
msgstr ""
msgid "Cancelling Preview"
msgstr ""
......@@ -19551,6 +19557,9 @@ msgstr ""
msgid "View open merge request"
msgstr ""
msgid "View previous app"
msgstr ""
msgid "View project labels"
msgstr ""
......
......@@ -96,7 +96,7 @@ describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page).to have_content("Failed to deploy to #{environment.name}")
expect(page).to have_content("Canceled deploy to #{environment.name}")
expect(page).not_to have_css('.js-deploy-time')
end
end
......
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
const 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',
details: {},
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/',
},
],
};
export default deploymentMockData;
import { mount } from '@vue/test-utils';
import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.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 DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue';
import {
CREATED,
RUNNING,
SUCCESS,
FAILED,
CANCELED,
} from '~/vue_merge_request_widget/components/deployment/constants';
import deploymentMockData 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', () => {
let wrapper;
const factory = (options = {}) => {
// This destroys any wrappers created before a nested call to factory reassigns it
if (wrapper && wrapper.destroy) {
wrapper.destroy();
}
wrapper = mount(DeploymentComponent, {
...options,
});
};
beforeEach(() => {
factory({
propsData: {
deployment: deploymentMockData,
showMetrics: false,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('always renders DeploymentInfo', () => {
expect(wrapper.find(DeploymentInfo).exists()).toBe(true);
});
describe('status message and buttons', () => {
const noActions = [];
const noDetails = { isManual: false };
const deployGroup = [DeploymentViewButton, DeploymentStopButton];
describe.each`
status | previous | deploymentDetails | text | actionButtons
${CREATED} | ${true} | ${deployDetail} | ${'Can deploy manually to'} | ${deployGroup}
${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup}
${CREATED} | ${false} | ${deployDetail} | ${'Can deploy manually to'} | ${noActions}
${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions}
${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup}
${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup}
${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions}
${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions}
${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup}
${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup}
${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup}
${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup}
${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions}
${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions}
${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deploy to'} | ${deployGroup}
${CANCELED} | ${true} | ${noDetails} | ${'Canceled deploy to'} | ${deployGroup}
${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deploy to'} | ${noActions}
${CANCELED} | ${false} | ${noDetails} | ${'Canceled deploy to'} | ${noActions}
`(
'$status + previous: $previous + manual: $deploymentDetails.isManual',
({ status, previous, deploymentDetails, text, actionButtons }) => {
beforeEach(() => {
const previousOrSuccess = Boolean(previous || status === SUCCESS);
const updatedDeploymentData = {
status,
deployed_at: previous ? deploymentMockData.deployed_at : null,
deployed_at_formatted: previous ? deploymentMockData.deployed_at_formatted : null,
external_url: previousOrSuccess ? deploymentMockData.external_url : null,
external_url_formatted: previousOrSuccess
? deploymentMockData.external_url_formatted
: null,
stop_url: previousOrSuccess ? deploymentMockData.stop_url : null,
details: deploymentDetails,
};
factory({
propsData: {
showMetrics: false,
deployment: {
...deploymentMockData,
...updatedDeploymentData,
},
},
});
});
it(`renders the text: ${text}`, () => {
expect(wrapper.find(DeploymentInfo).text()).toContain(text);
});
if (actionButtons.length > 0) {
describe('renders the expected button group', () => {
actionButtons.forEach(button => {
it(`renders ${button.name}`, () => {
expect(wrapper.find(button).exists()).toBe(true);
});
});
});
}
if (actionButtons.length === 0) {
describe('does not render the button group', () => {
[DeploymentViewButton, DeploymentStopButton].forEach(button => {
it(`does not render ${button.name}`, () => {
expect(wrapper.find(button).exists()).toBe(false);
});
});
});
}
if (actionButtons.includes(DeploymentViewButton)) {
it('renders the View button with expected text', () => {
if (status === SUCCESS) {
expect(wrapper.find(DeploymentViewButton).text()).toContain('View app');
} else {
expect(wrapper.find(DeploymentViewButton).text()).toContain('View previous app');
}
});
}
},
);
});
describe('hasExternalUrls', () => {
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', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(true);
});
});
describe('when deployment has no external_url_formatted', () => {
beforeEach(() => {
factory({
propsData: {
deployment: { ...deploymentMockData, external_url_formatted: null },
showMetrics: false,
},
});
});
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
});
});
describe('when deployment has no external_url', () => {
beforeEach(() => {
factory({
propsData: {
deployment: { ...deploymentMockData, external_url: null },
showMetrics: false,
},
});
});
it('should return false', () => {
expect(wrapper.vm.hasExternalUrls).toEqual(false);
});
it('should not render the View Button', () => {
expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
});
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
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 deploymentMockData from './deployment_mock_data';
describe('Deployment View App button', () => {
let wrapper;
const factory = (options = {}) => {
const localVue = createLocalVue();
wrapper = mount(localVue.extend(DeploymentViewButton), {
localVue,
...options,
});
};
beforeEach(() => {
factory({
propsData: {
deployment: deploymentMockData,
isCurrent: true,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('text', () => {
describe('when app is current', () => {
it('shows View app', () => {
expect(wrapper.find(ReviewAppLink).text()).toContain('View app');
});
});
describe('when app is not current', () => {
beforeEach(() => {
factory({
propsData: {
deployment: deploymentMockData,
isCurrent: false,
},
});
});
it('shows View Previous app', () => {
expect(wrapper.find(ReviewAppLink).text()).toContain('View previous app');
});
});
});
describe('without changes', () => {
beforeEach(() => {
factory({
propsData: {
deployment: { ...deploymentMockData, changes: null },
isCurrent: false,
},
});
});
it('renders the link to the review app without dropdown', () => {
expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
});
});
describe('with a single change', () => {
beforeEach(() => {
factory({
propsData: {
deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
isCurrent: false,
},
});
});
it('renders the link to the review app without dropdown', () => {
expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
});
it('renders the link to the review app linked to to the first change', () => {
const expectedUrl = deploymentMockData.changes[0].external_url;
const deployUrl = wrapper.find('.js-deploy-url');
expect(deployUrl.attributes().href).not.toBeNull();
expect(deployUrl.attributes().href).toEqual(expectedUrl);
});
});
describe('with multiple changes', () => {
beforeEach(() => {
factory({
propsData: {
deployment: deploymentMockData,
isCurrent: false,
},
});
});
it('renders the link to the review app with dropdown', () => {
expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(true);
});
it('renders all the links to the review apps', () => {
const allUrls = wrapper.findAll('.js-deploy-url-menu-item').wrappers;
const expectedUrls = deploymentMockData.changes.map(change => change.external_url);
expectedUrls.forEach((expectedUrl, idx) => {
const deployUrl = allUrls[idx];
expect(deployUrl.attributes().href).not.toBeNull();
expect(deployUrl.attributes().href).toEqual(expectedUrl);
});
});
});
});
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Deployment component', () => {
const Component = Vue.extend(deploymentComponent);
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',
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, { deployment: { ...deploymentMockData }, showMetrics: true });
});
describe('deployTimeago', () => {
it('return formatted date', () => {
const readable = getTimeago().format(deploymentMockData.deployed_at);
expect(vm.deployTimeago).toEqual(readable);
});
});
describe('hasExternalUrls', () => {
it('should return true', () => {
expect(vm.hasExternalUrls).toEqual(true);
});
it('should return false when deployment has no external_url_formatted', () => {
vm.deployment.external_url_formatted = null;
expect(vm.hasExternalUrls).toEqual(false);
});
it('should return false when deployment has no external_url', () => {
vm.deployment.external_url = null;
expect(vm.hasExternalUrls).toEqual(false);
});
});
describe('hasDeploymentTime', () => {
it('should return true', () => {
expect(vm.hasDeploymentTime).toEqual(true);
});
it('should return false when deployment has no deployed_at', () => {
vm.deployment.deployed_at = null;
expect(vm.hasDeploymentTime).toEqual(false);
});
it('should return false when deployment has no deployed_at_formatted', () => {
vm.deployment.deployed_at_formatted = null;
expect(vm.hasDeploymentTime).toEqual(false);
});
});
describe('hasDeploymentMeta', () => {
it('should return true', () => {
expect(vm.hasDeploymentMeta).toEqual(true);
});
it('should return false when deployment has no url', () => {
vm.deployment.url = null;
expect(vm.hasDeploymentMeta).toEqual(false);
});
it('should return false when deployment has no name', () => {
vm.deployment.name = null;
expect(vm.hasDeploymentMeta).toEqual(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(deploymentComponent, '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();
});
});
it('renders deployment name', () => {
expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
deploymentMockData.url,
);
expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
});
it('renders external URL', () => {
expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
deploymentMockData.external_url,
);
expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
expect(vm.$el.querySelector('.btn')).not.toBeNull();
});
it('renders deployment time', () => {
expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
});
it('renders metrics component', () => {
expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull();
});
});
describe('with showMetrics enabled', () => {
beforeEach(() => {
vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
});
it('shows metrics', () => {
expect(vm.$el).toContainElement('.js-mr-memory-usage');
});
});
describe('with showMetrics disabled', () => {
beforeEach(() => {
vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: false });
});
it('hides metrics', () => {
expect(vm.$el).not.toContainElement('.js-mr-memory-usage');
});
});
describe('without changes', () => {
beforeEach(() => {
delete deploymentMockData.changes;
vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true });
});
it('renders the link to the review app without dropdown', () => {
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
});
});
describe('with a single change', () => {
beforeEach(() => {
deploymentMockData.changes = deploymentMockData.changes.slice(0, 1);
vm = mountComponent(Component, {
deployment: { ...deploymentMockData },
showMetrics: true,
});
});
it('renders the link to the review app without dropdown', () => {
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
});
it('renders the link to the review app linked to to the first change', () => {
const expectedUrl = deploymentMockData.changes[0].external_url;
const deployUrl = vm.$el.querySelector('.js-deploy-url');
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(deployUrl).not.toBeNull();
expect(deployUrl.href).toEqual(expectedUrl);
});
});
describe('deployment status', () => {
describe('running', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'running' }),
showMetrics: true,
});
});
it('renders information about running deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to');
});
it('renders disabled stop button', () => {
expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled');
});
});
describe('success', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'success' }),
showMetrics: true,
});
});
it('renders information about finished deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to');
});
});
describe('failed', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'failed' }),
showMetrics: true,
});
});
it('renders information about finished deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
'Failed to deploy to',
);
});
});
describe('created', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'created' }),
showMetrics: true,
});
});
it('renders information about created deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Will deploy to');
});
});
describe('canceled', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'canceled' }),
showMetrics: true,
});
});
it('renders information about canceled deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
'Failed to deploy to',
);
});
});
});
});
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();
});
});
});
});
import Vue from 'vue';
import MemoryUsage from '~/vue_merge_request_widget/components/memory_usage.vue';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
......
......@@ -8,6 +8,7 @@ describe('review app link', () => {
const props = {
link: '/review',
cssClass: 'js-link',
isCurrent: true,
};
let vm;
let el;
......
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
export default {
id: 132,
iid: 22,
......@@ -290,15 +292,20 @@ export const mockStore = {
name: 'bogus',
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
status: SUCCESS,
},
{
id: 1,
name: 'bogus-docs',
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
status: SUCCESS,
},
],
postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }],
postMergeDeployments: [
{ id: 0, name: 'prod', status: SUCCESS },
{ id: 1, name: 'prod-docs', status: SUCCESS },
],
troubleshootingDocsPath: 'troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
......
......@@ -6,6 +6,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
const returnPromise = data =>
new Promise(resolve => {
......@@ -277,7 +278,9 @@ describe('mrWidgetOptions', () => {
describe('fetchDeployments', () => {
it('should fetch deployments', done => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
spyOn(vm.service, 'fetchDeployments').and.returnValue(
returnPromise([{ id: 1, status: SUCCESS }]),
);
vm.fetchPreMergeDeployments();
......@@ -554,7 +557,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
status: 'success',
status: SUCCESS,
};
beforeEach(done => {
......
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