Commit 0f780730 authored by Sarah GP's avatar Sarah GP

Implement frontend for auto-stop envs

This is the commit for all changes, for merging

This change involves both HAML and Vue, plus refactoring
parent db3e5019
<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';
import _ from 'underscore';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentItem from './environment_item.vue';
export default {
......@@ -16,7 +17,7 @@ export default {
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
},
mixins: [environmentTableMixin],
mixins: [environmentTableMixin, glFeatureFlagsMixin()],
props: {
environments: {
type: Array,
......@@ -42,6 +43,9 @@ export default {
: env,
);
},
shouldShowAutoStopDate() {
return this.glFeatures.autoStopEnvironments;
},
tableData() {
return {
// percent spacing for cols, should add up to 100
......@@ -65,8 +69,12 @@ export default {
title: s__('Environments|Updated'),
spacing: 'section-10',
},
autoStop: {
title: s__('Environments|Auto stop in'),
spacing: 'section-5',
},
actions: {
spacing: 'section-30',
spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
},
};
},
......@@ -123,6 +131,14 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }}
</div>
<div
v-if="shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="columnheader"
>
{{ tableData.autoStop.title }}
</div>
</div>
<template v-for="(model, i) in sortedEnvironments" :model="model">
<div
......@@ -130,6 +146,7 @@ export default {
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
:should-show-auto-stop-date="shouldShowAutoStopDate"
:table-data="tableData"
/>
......
......@@ -90,16 +90,19 @@ export default {
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) {
this.isLoading = true;
this.service
.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => {
.catch(err => {
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 {
);
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: {
......@@ -199,6 +209,8 @@ export default {
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
},
beforeDestroy() {
......@@ -208,5 +220,7 @@ export default {
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
},
};
......@@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
end
before_action do
push_frontend_feature_flag(:auto_stop_environments)
end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
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 @@
= button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do
= s_('Environments|Stop environment')
.top-area
.top-area.justify-content-between
.d-flex
%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/external_url', 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 ""
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
......@@ -6834,6 +6837,12 @@ msgstr ""
msgid "Environments|Are you sure you want to stop this environment?"
msgstr ""
msgid "Environments|Auto stop in"
msgstr ""
msgid "Environments|Auto stops %{auto_stop_time}"
msgstr ""
msgid "Environments|Commit"
msgstr ""
......@@ -13326,6 +13335,9 @@ msgstr ""
msgid "Prevent approval of merge requests by merge request committers"
msgstr ""
msgid "Prevent environment from auto-stopping"
msgstr ""
msgid "Preview"
msgstr ""
......
......@@ -12,6 +12,10 @@ describe 'Environment' do
project.add_role(user, role)
end
def auto_stop_button_selector
%q{button[title="Prevent environment from auto-stopping"]}
end
describe 'environment details page' do
let!(:environment) { create(:environment, project: project) }
let!(:permissions) { }
......@@ -27,6 +31,40 @@ describe 'Environment' do
expect(page).to have_content(environment.name)
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
it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
......
import { mount } from '@vue/test-utils';
import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
import { environment, folder, tableData } from './mock_data';
describe('Environment item', () => {
......@@ -26,6 +28,8 @@ describe('Environment item', () => {
});
});
const findAutoStop = () => wrapper.find('.js-auto-stop');
afterEach(() => {
wrapper.destroy();
});
......@@ -77,6 +81,79 @@ describe('Environment item', () => {
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', () => {
......
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 = {
log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
auto_stop_at: null,
};
const folder = {
......@@ -98,6 +99,10 @@ const tableData = {
title: 'Updated',
spacing: 'section-10',
},
autoStop: {
title: 'Auto stop in',
spacing: 'section-5',
},
actions: {
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