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>
/**
* 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
.d-flex
%h3.page-title= @environment.name %h3.page-title= @environment.name
.nav-controls.ml-auto.my-2 - 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