Commit 40b9bf8f authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '20956-autostop-frontend' into 'master'

Resolve "Auto stop environments after a certain period"

See merge request gitlab-org/gitlab!20372
parents acb38fa0 0f780730
<script> <script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */ /* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { format } from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin'; import { __, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { __, sprintf } from '~/locale'; import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue'; import RollbackComponent from './environment_rollback.vue';
import StopComponent from './environment_stop.vue';
import TerminalButtonComponent from './environment_terminal_button.vue'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
/** /**
* Environment Item Component * Environment Item Component
...@@ -26,21 +27,22 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; ...@@ -26,21 +27,22 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
CommitComponent,
Icon,
ActionsComponent, ActionsComponent,
CommitComponent,
ExternalUrlComponent, ExternalUrlComponent,
StopComponent, Icon,
MonitoringButtonComponent,
PinComponent,
RollbackComponent, RollbackComponent,
StopComponent,
TerminalButtonComponent, TerminalButtonComponent,
MonitoringButtonComponent,
TooltipOnTruncate, TooltipOnTruncate,
UserAvatarLink, UserAvatarLink,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [environmentItemMixin], mixins: [environmentItemMixin, timeagoMixin],
props: { props: {
canReadEnvironment: { canReadEnvironment: {
...@@ -52,7 +54,12 @@ export default { ...@@ -52,7 +54,12 @@ export default {
model: { model: {
type: Object, type: Object,
required: true, required: true,
default: () => ({}), },
shouldShowAutoStopDate: {
type: Boolean,
required: false,
default: false,
}, },
tableData: { tableData: {
...@@ -76,6 +83,16 @@ export default { ...@@ -76,6 +83,16 @@ export default {
return false; return false;
}, },
/**
* Checkes whether the row displayed is a folder.
*
* @returns {Boolean}
*/
isFolder() {
return this.model.isFolder;
},
/** /**
* Checkes whether the environment is protected. * Checkes whether the environment is protected.
* (`is_protected` currently only set in EE) * (`is_protected` currently only set in EE)
...@@ -112,24 +129,64 @@ export default { ...@@ -112,24 +129,64 @@ export default {
}, },
/** /**
* Verifies if the date to be shown is present. * Verifies if the autostop date is present.
*
* @returns {Boolean}
*/
canShowAutoStopDate() {
if (!this.model.auto_stop_at) {
return false;
}
const autoStopDate = new Date(this.model.auto_stop_at);
const now = new Date();
return now < autoStopDate;
},
/**
* Human readable deployment date.
*
* @returns {String}
*/
autoStopDate() {
if (this.canShowAutoStopDate) {
return {
formatted: this.timeFormatted(this.model.auto_stop_at),
tooltip: this.tooltipTitle(this.model.auto_stop_at),
};
}
return {
formatted: '',
tooltip: '',
};
},
/**
* Verifies if the deployment date is present.
* *
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
canShowDate() { canShowDeploymentDate() {
return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at; return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
}, },
/** /**
* Human readable date. * Human readable deployment date.
* *
* @returns {String} * @returns {String}
*/ */
deployedDate() { deployedDate() {
if (this.canShowDate) { if (this.canShowDeploymentDate) {
return format(this.model.last_deployment.deployed_at); return {
formatted: this.timeFormatted(this.model.last_deployment.deployed_at),
tooltip: this.tooltipTitle(this.model.last_deployment.deployed_at),
};
} }
return ''; return {
formatted: '',
tooltip: '',
};
}, },
actions() { actions() {
...@@ -344,6 +401,15 @@ export default { ...@@ -344,6 +401,15 @@ export default {
return {}; return {};
}, },
/**
* Checkes whether to display no deployment text.
*
* @returns {Boolean}
*/
showNoDeployments() {
return !this.hasLastDeploymentKey && !this.isFolder;
},
/** /**
* Verifies if the build name column should be rendered by verifing * Verifies if the build name column should be rendered by verifing
* if all the information needed is present * if all the information needed is present
...@@ -353,7 +419,7 @@ export default { ...@@ -353,7 +419,7 @@ export default {
*/ */
shouldRenderBuildName() { shouldRenderBuildName() {
return ( return (
!this.model.isFolder && !this.isFolder &&
!_.isEmpty(this.model.last_deployment) && !_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable) !_.isEmpty(this.model.last_deployment.deployable)
); );
...@@ -383,11 +449,7 @@ export default { ...@@ -383,11 +449,7 @@ export default {
* @return {String} * @return {String}
*/ */
externalURL() { externalURL() {
if (this.model && this.model.external_url) { return this.model.external_url || '';
return this.model.external_url;
}
return '';
}, },
/** /**
...@@ -399,26 +461,22 @@ export default { ...@@ -399,26 +461,22 @@ export default {
*/ */
shouldRenderDeploymentID() { shouldRenderDeploymentID() {
return ( return (
!this.model.isFolder && !this.isFolder &&
!_.isEmpty(this.model.last_deployment) && !_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined this.model.last_deployment.iid !== undefined
); );
}, },
environmentPath() { environmentPath() {
if (this.model && this.model.environment_path) { return this.model.environment_path || '';
return this.model.environment_path;
}
return '';
}, },
monitoringUrl() { monitoringUrl() {
if (this.model && this.model.metrics_path) { return this.model.metrics_path || '';
return this.model.metrics_path; },
}
return ''; autoStopUrl() {
return this.model.cancel_auto_stop_path || '';
}, },
displayEnvironmentActions() { displayEnvironmentActions() {
...@@ -447,7 +505,7 @@ export default { ...@@ -447,7 +505,7 @@ export default {
<div <div
:class="{ :class="{
'js-child-row environment-child-row': model.isChildren, 'js-child-row environment-child-row': model.isChildren,
'folder-row': model.isFolder, 'folder-row': isFolder,
}" }"
class="gl-responsive-table-row" class="gl-responsive-table-row"
role="row" role="row"
...@@ -457,7 +515,7 @@ export default { ...@@ -457,7 +515,7 @@ export default {
:class="tableData.name.spacing" :class="tableData.name.spacing"
role="gridcell" role="gridcell"
> >
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader"> <div v-if="!isFolder" class="table-mobile-header" role="rowheader">
{{ tableData.name.title }} {{ tableData.name.title }}
</div> </div>
...@@ -466,7 +524,7 @@ export default { ...@@ -466,7 +524,7 @@ export default {
</span> </span>
<span <span
v-if="!model.isFolder" v-if="!isFolder"
v-gl-tooltip v-gl-tooltip
:title="model.name" :title="model.name"
class="environment-name table-mobile-content" class="environment-name table-mobile-content"
...@@ -506,7 +564,7 @@ export default { ...@@ -506,7 +564,7 @@ export default {
{{ deploymentInternalId }} {{ deploymentInternalId }}
</span> </span>
<span v-if="!model.isFolder && deploymentHasUser" class="text-break-word"> <span v-if="!isFolder && deploymentHasUser" class="text-break-word">
by by
<user-avatar-link <user-avatar-link
:link-href="deploymentUser.web_url" :link-href="deploymentUser.web_url"
...@@ -516,6 +574,10 @@ export default { ...@@ -516,6 +574,10 @@ export default {
class="js-deploy-user-container float-none" class="js-deploy-user-container float-none"
/> />
</span> </span>
<div v-if="showNoDeployments" class="commit-title table-mobile-content">
{{ s__('Environments|No deployments yet') }}
</div>
</div> </div>
<div <div
...@@ -536,14 +598,8 @@ export default { ...@@ -536,14 +598,8 @@ export default {
</a> </a>
</div> </div>
<div <div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
v-if="!model.isFolder"
class="table-section"
:class="tableData.commit.spacing"
role="gridcell"
>
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div> <div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content"> <div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
...@@ -554,31 +610,51 @@ export default { ...@@ -554,31 +610,51 @@ export default {
:author="commitAuthor" :author="commitAuthor"
/> />
</div> </div>
<div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content"> </div>
{{ s__('Environments|No deployments yet') }}
</div> <div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
<span
v-if="canShowDeploymentDate"
v-gl-tooltip
:title="deployedDate.tooltip"
class="environment-created-date-timeago table-mobile-content flex-truncate-parent"
>
<span class="flex-truncate-child">
{{ deployedDate.formatted }}
</span>
</span>
</div> </div>
<div <div
v-if="!model.isFolder" v-if="!isFolder && shouldShowAutoStopDate"
class="table-section" class="table-section"
:class="tableData.date.spacing" :class="tableData.autoStop.spacing"
role="gridcell" role="gridcell"
> >
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div> <div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
<span
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> v-if="canShowAutoStopDate"
{{ deployedDate }} v-gl-tooltip
:title="autoStopDate.tooltip"
class="table-mobile-content flex-truncate-parent"
>
<span class="flex-truncate-child js-auto-stop">{{ autoStopDate.formatted }}</span>
</span> </span>
</div> </div>
<div <div
v-if="!model.isFolder && displayEnvironmentActions" v-if="!isFolder && displayEnvironmentActions"
class="table-section table-button-footer" class="table-section table-button-footer"
:class="tableData.actions.spacing" :class="tableData.actions.spacing"
role="gridcell" role="gridcell"
> >
<div class="btn-group table-action-buttons" role="group"> <div class="btn-group table-action-buttons" role="group">
<pin-component
v-if="canShowAutoStopDate && shouldShowAutoStopDate"
:auto-stop-url="autoStopUrl"
/>
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL && canReadEnvironment"
:external-url="externalURL" :external-url="externalURL"
......
<script>
/**
* Renders a prevent auto-stop button.
* Used in environments table.
*/
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
Icon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
autoStopUrl: {
type: String,
required: true,
},
},
methods: {
onPinClick() {
eventHub.$emit('cancelAutoStop', this.autoStopUrl);
},
},
title: __('Prevent environment from auto-stopping'),
};
</script>
<template>
<gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
<icon name="thumbtack" />
</gl-button>
</template>
...@@ -6,6 +6,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -6,6 +6,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore'; import _ from 'underscore';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin'; import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentItem from './environment_item.vue'; import EnvironmentItem from './environment_item.vue';
export default { export default {
...@@ -16,7 +17,7 @@ export default { ...@@ -16,7 +17,7 @@ export default {
CanaryDeploymentCallout: () => CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'), import('ee_component/environments/components/canary_deployment_callout.vue'),
}, },
mixins: [environmentTableMixin], mixins: [environmentTableMixin, glFeatureFlagsMixin()],
props: { props: {
environments: { environments: {
type: Array, type: Array,
...@@ -42,6 +43,9 @@ export default { ...@@ -42,6 +43,9 @@ export default {
: env, : env,
); );
}, },
shouldShowAutoStopDate() {
return this.glFeatures.autoStopEnvironments;
},
tableData() { tableData() {
return { return {
// percent spacing for cols, should add up to 100 // percent spacing for cols, should add up to 100
...@@ -65,8 +69,12 @@ export default { ...@@ -65,8 +69,12 @@ export default {
title: s__('Environments|Updated'), title: s__('Environments|Updated'),
spacing: 'section-10', spacing: 'section-10',
}, },
autoStop: {
title: s__('Environments|Auto stop in'),
spacing: 'section-5',
},
actions: { actions: {
spacing: 'section-30', spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
}, },
}; };
}, },
...@@ -123,6 +131,14 @@ export default { ...@@ -123,6 +131,14 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader"> <div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }} {{ tableData.date.title }}
</div> </div>
<div
v-if="shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="columnheader"
>
{{ tableData.autoStop.title }}
</div>
</div> </div>
<template v-for="(model, i) in sortedEnvironments" :model="model"> <template v-for="(model, i) in sortedEnvironments" :model="model">
<div <div
...@@ -130,6 +146,7 @@ export default { ...@@ -130,6 +146,7 @@ export default {
:key="`environment-item-${i}`" :key="`environment-item-${i}`"
:model="model" :model="model"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:should-show-auto-stop-date="shouldShowAutoStopDate"
:table-data="tableData" :table-data="tableData"
/> />
......
...@@ -90,16 +90,19 @@ export default { ...@@ -90,16 +90,19 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.')); Flash(s__('Environments|An error occurred while fetching the environments.'));
}, },
postAction({ endpoint, errorMessage }) { postAction({
endpoint,
errorMessage = s__('Environments|An error occurred while making the request.'),
}) {
if (!this.isMakingRequest) { if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
this.service this.service
.postAction(endpoint) .postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => { .catch(err => {
this.isLoading = false; this.isLoading = false;
Flash(errorMessage || s__('Environments|An error occurred while making the request.')); Flash(_.isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage);
}); });
} }
}, },
...@@ -138,6 +141,13 @@ export default { ...@@ -138,6 +141,13 @@ export default {
); );
this.postAction({ endpoint: retryUrl, errorMessage }); this.postAction({ endpoint: retryUrl, errorMessage });
}, },
cancelAutoStop(autoStopPath) {
const errorMessage = ({ message }) =>
message ||
s__('Environments|An error occurred while canceling the auto stop, please try again');
this.postAction({ endpoint: autoStopPath, errorMessage });
},
}, },
computed: { computed: {
...@@ -199,6 +209,8 @@ export default { ...@@ -199,6 +209,8 @@ export default {
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal); eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment); eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
}, },
beforeDestroy() { beforeDestroy() {
...@@ -208,5 +220,7 @@ export default { ...@@ -208,5 +220,7 @@ export default {
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal); eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment); eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
}, },
}; };
...@@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
end end
before_action do
push_frontend_feature_flag(:auto_stop_environments)
end
after_action :expire_etag_cache, only: [:cancel_auto_stop] after_action :expire_etag_cache, only: [:cancel_auto_stop]
def index def index
......
- if environment.auto_stop_at? && environment.available?
= button_to cancel_auto_stop_project_environment_path(environment.project, environment), class: 'btn btn-secondary has-tooltip', title: _('Prevent environment from auto-stopping') do
= sprite_icon('thumbtack')
...@@ -32,9 +32,14 @@ ...@@ -32,9 +32,14 @@
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment') = s_('Environments|Stop environment')
.top-area .top-area.justify-content-between
%h3.page-title= @environment.name .d-flex
.nav-controls.ml-auto.my-2 %h3.page-title= @environment.name
- if @environment.auto_stop_at?
%p.align-self-end.prepend-left-8
= s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
.nav-controls.my-2
= render 'projects/environments/pin_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment = render 'projects/environments/external_url', environment: @environment
= render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/metrics_button', environment: @environment
......
---
title: Auto stop environments after a certain period
merge_request: 20372
author:
type: added
...@@ -6816,6 +6816,9 @@ msgstr "" ...@@ -6816,6 +6816,9 @@ msgstr ""
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses." msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
msgstr "" msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
msgid "Environments|An error occurred while fetching the environments." msgid "Environments|An error occurred while fetching the environments."
msgstr "" msgstr ""
...@@ -6834,6 +6837,12 @@ msgstr "" ...@@ -6834,6 +6837,12 @@ msgstr ""
msgid "Environments|Are you sure you want to stop this environment?" msgid "Environments|Are you sure you want to stop this environment?"
msgstr "" msgstr ""
msgid "Environments|Auto stop in"
msgstr ""
msgid "Environments|Auto stops %{auto_stop_time}"
msgstr ""
msgid "Environments|Commit" msgid "Environments|Commit"
msgstr "" msgstr ""
...@@ -13329,6 +13338,9 @@ msgstr "" ...@@ -13329,6 +13338,9 @@ msgstr ""
msgid "Prevent approval of merge requests by merge request committers" msgid "Prevent approval of merge requests by merge request committers"
msgstr "" msgstr ""
msgid "Prevent environment from auto-stopping"
msgstr ""
msgid "Preview" msgid "Preview"
msgstr "" msgstr ""
......
...@@ -12,6 +12,10 @@ describe 'Environment' do ...@@ -12,6 +12,10 @@ describe 'Environment' do
project.add_role(user, role) project.add_role(user, role)
end end
def auto_stop_button_selector
%q{button[title="Prevent environment from auto-stopping"]}
end
describe 'environment details page' do describe 'environment details page' do
let!(:environment) { create(:environment, project: project) } let!(:environment) { create(:environment, project: project) }
let!(:permissions) { } let!(:permissions) { }
...@@ -27,6 +31,40 @@ describe 'Environment' do ...@@ -27,6 +31,40 @@ describe 'Environment' do
expect(page).to have_content(environment.name) expect(page).to have_content(environment.name)
end end
context 'without auto-stop' do
it 'does not show auto-stop text' do
expect(page).not_to have_content('Auto stops')
end
it 'does not show auto-stop button' do
expect(page).not_to have_selector(auto_stop_button_selector)
end
end
context 'with auto-stop' do
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
before do
visit_environment(environment)
end
it 'shows auto stop info' do
expect(page).to have_content('Auto stops')
end
it 'shows auto stop button' do
expect(page).to have_selector(auto_stop_button_selector)
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
end
it 'allows user to cancel auto stop', :js do
page.find(auto_stop_button_selector).click
wait_for_all_requests
expect(page).to have_content('Auto stop successfully canceled.')
expect(page).not_to have_selector(auto_stop_button_selector)
end
end
context 'without deployments' do context 'without deployments' do
it 'does not show deployments' do it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.') expect(page).to have_content('You don\'t have any deployments right now.')
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { format } from 'timeago.js'; import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue'; import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
import { environment, folder, tableData } from './mock_data'; import { environment, folder, tableData } from './mock_data';
describe('Environment item', () => { describe('Environment item', () => {
...@@ -26,6 +28,8 @@ describe('Environment item', () => { ...@@ -26,6 +28,8 @@ describe('Environment item', () => {
}); });
}); });
const findAutoStop = () => wrapper.find('.js-auto-stop');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -77,6 +81,79 @@ describe('Environment item', () => { ...@@ -77,6 +81,79 @@ describe('Environment item', () => {
expect(wrapper.find('.js-commit-component')).toBeDefined(); expect(wrapper.find('.js-commit-component')).toBeDefined();
}); });
}); });
describe('Without auto-stop date', () => {
beforeEach(() => {
factory({
propsData: {
model: environment,
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
});
});
it('should not render a date', () => {
expect(findAutoStop().exists()).toBe(false);
});
it('should not render the suto-stop button', () => {
expect(wrapper.find(PinComponent).exists()).toBe(false);
});
});
describe('With auto-stop date', () => {
describe('in the future', () => {
const futureDate = new Date(Date.now() + 100000);
beforeEach(() => {
factory({
propsData: {
model: {
...environment,
auto_stop_at: futureDate,
},
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
});
});
it('renders the date', () => {
expect(findAutoStop().text()).toContain(format(futureDate));
});
it('should render the auto-stop button', () => {
expect(wrapper.find(PinComponent).exists()).toBe(true);
});
});
describe('in the past', () => {
const pastDate = new Date(Date.now() - 100000);
beforeEach(() => {
factory({
propsData: {
model: {
...environment,
auto_stop_at: pastDate,
},
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
});
});
it('should not render a date', () => {
expect(findAutoStop().exists()).toBe(false);
});
it('should not render the suto-stop button', () => {
expect(wrapper.find(PinComponent).exists()).toBe(false);
});
});
});
}); });
describe('With manual actions', () => { describe('With manual actions', () => {
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/environments/event_hub';
import PinComponent from '~/environments/components/environment_pin.vue';
describe('Pin 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 = shallowMount(PinComponent, {
...options,
});
};
const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
beforeEach(() => {
factory({
propsData: {
autoStopUrl,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('should render the component with thumbtack icon', () => {
expect(wrapper.find(Icon).props('name')).toBe('thumbtack');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const button = wrapper.find(GlButton);
button.vm.$emit('click');
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
});
});
...@@ -63,6 +63,7 @@ const environment = { ...@@ -63,6 +63,7 @@ const environment = {
log_path: 'root/ci-folders/environments/31/logs', log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z', updated_at: '2016-11-10T15:55:58.778Z',
auto_stop_at: null,
}; };
const folder = { const folder = {
...@@ -98,6 +99,10 @@ const tableData = { ...@@ -98,6 +99,10 @@ const tableData = {
title: 'Updated', title: 'Updated',
spacing: 'section-10', spacing: 'section-10',
}, },
autoStop: {
title: 'Auto stop in',
spacing: 'section-5',
},
actions: { actions: {
spacing: 'section-25', spacing: 'section-25',
}, },
......
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