Commit 2526d2dc authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch '20450-fix-ujs-actions' into 'master'

Use a button and a post request instead of UJS links - part 1 - Environments

See merge request !9688
parents 4b4e1f04 cbf1b656
Pipeline #6998864 failed with stages
in 4 minutes and 55 seconds
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
import eventHub from '../event_hub';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', {
export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
......@@ -66,33 +67,15 @@ module.exports = Vue.component('environment-component', {
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
this.service = new EnvironmentsService(this.endpoint);
this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
},
methods: {
......@@ -112,6 +95,32 @@ module.exports = Vue.component('environment-component', {
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.');
});
},
},
template: `
......@@ -144,7 +153,7 @@ module.exports = Vue.component('environment-component', {
<div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon"
......@@ -173,7 +182,8 @@ module.exports = Vue.component('environment-component', {
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"/>
:can-read-environment="canReadEnvironmentParsed"
:service="service"/>
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
const Vue = require('vue');
const playIconSvg = require('icons/_icon_play.svg');
/* global Flash */
/* eslint-disable no-new */
module.exports = Vue.component('actions-component', {
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
export default {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
service: {
type: Object,
required: true,
},
},
data() {
return { playIconSvg };
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<div class="btn-group" role="group">
<button class="dropdown btn btn-default dropdown-new" data-toggle="dropdown">
<button
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-toggle="dropdown"
:disabled="isLoading">
<span>
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
<span v-html="playIconSvg"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</span>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<a :href="action.play_path"
data-method="post"
rel="nofollow"
class="js-manual-action-link">
<button
@click="onClickAction(action.play_path)"
class="js-manual-action-link no-btn">
${playIconSvg}
<span>
{{action.name}}
</span>
</a>
</button>
</li>
</ul>
</button>
</div>
`,
});
};
/**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
module.exports = Vue.component('external-url-component', {
export default {
props: {
externalUrl: {
type: String,
......@@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', {
},
template: `
<a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i>
<a
class="btn external_url"
:href="externalUrl"
target="_blank"
title="Environment external URL">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
});
};
const Vue = require('vue');
const Timeago = require('timeago.js');
require('../../lib/utils/text_utility');
require('../../vue_shared/components/commit');
const ActionsComponent = require('./environment_actions');
const ExternalUrlComponent = require('./environment_external_url');
const StopComponent = require('./environment_stop');
const RollbackComponent = require('./environment_rollback');
const TerminalButtonComponent = require('./environment_terminal_button');
import Timeago from 'timeago.js';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
import '../../lib/utils/text_utility';
import '../../vue_shared/components/commit';
/**
* Envrionment Item Component
......@@ -17,7 +15,7 @@ const TerminalButtonComponent = require('./environment_terminal_button');
const timeagoInstance = new Timeago();
module.exports = Vue.component('environment-item', {
export default {
components: {
'commit-component': gl.CommitComponent,
......@@ -46,6 +44,11 @@ module.exports = Vue.component('environment-item', {
required: false,
default: false,
},
service: {
type: Object,
required: true,
},
},
computed: {
......@@ -489,22 +492,25 @@ module.exports = Vue.component('environment-item', {
<td class="environments-actions">
<div v-if="!model.isFolder" class="btn-group pull-right" role="group">
<actions-component v-if="hasManualActions && canCreateDeployment"
:service="service"
:actions="manualActions"/>
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"/>
:stop-url="model.stop_path"
:service="service"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"/>
:retry-url="retryUrl"
:service="service"/>
</div>
</td>
</tr>
`,
});
};
/* global Flash */
/* eslint-disable no-new */
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
const Vue = require('vue');
import eventHub from '../event_hub';
module.exports = Vue.component('rollback-component', {
export default {
props: {
retryUrl: {
type: String,
......@@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', {
type: Boolean,
default: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
this.isLoading = true;
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<button type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
</a>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`,
});
};
/* global Flash */
/* eslint-disable no-new, no-alert */
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
import eventHub from '../event_hub';
module.exports = Vue.component('stop-component', {
export default {
props: {
stopUrl: {
type: String,
default: '',
},
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.', 'alert');
});
}
},
},
template: `
<a class="btn stop-env-link"
:href="stopUrl"
data-confirm="Are you sure you want to stop this environment?"
data-method="post"
rel="nofollow">
<button type="button"
class="btn stop-env-link"
@click="onClick"
:disabled="isLoading"
title="Stop Environment">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`,
});
};
......@@ -2,13 +2,13 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
const terminalIconSvg = require('icons/_icon_terminal.svg');
import terminalIconSvg from 'icons/_icon_terminal.svg';
module.exports = Vue.component('terminal-button-component', {
export default {
props: {
terminalPath: {
type: String,
required: false,
default: '',
},
},
......@@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', {
template: `
<a class="btn terminal-button"
title="Open web terminal"
:href="terminalPath">
${terminalIconSvg}
</a>
`,
});
};
/**
* Render environments table.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
module.exports = Vue.component('environment-table-component', {
import EnvironmentItem from './environment_item';
export default {
components: {
'environment-item': EnvironmentItem,
},
......@@ -28,6 +26,11 @@ module.exports = Vue.component('environment-table-component', {
required: false,
default: false,
},
service: {
type: Object,
required: true,
},
},
template: `
......@@ -48,9 +51,10 @@ module.exports = Vue.component('environment-table-component', {
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"></tr>
:can-read-environment="canReadEnvironment"
:service="service"></tr>
</template>
</tbody>
</table>
`,
});
};
const EnvironmentsComponent = require('./components/environment');
import EnvironmentsComponent from './components/environment';
$(() => {
window.gl = window.gl || {};
......
import Vue from 'vue';
export default new Vue();
const EnvironmentsFolderComponent = require('./environments_folder_view');
import EnvironmentsFolderComponent from './environments_folder_view';
$(() => {
window.gl = window.gl || {};
......
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', {
export default Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
......@@ -88,11 +88,11 @@ module.exports = Vue.component('environment-folder-view', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.get()
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
......@@ -168,13 +168,12 @@ module.exports = Vue.component('environment-folder-view', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
:commit-icon-svg="commitIconSvg"
:service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
:pageInfo="state.paginationInformation"/>
</div>
</div>
</div>
......
const Vue = require('vue');
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
class EnvironmentsService {
export default class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
}
get() {
return this.environments.get();
get(scope, page) {
return this.environments.get({ scope, page });
}
}
module.exports = EnvironmentsService;
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
}
require('~/lib/utils/common_utils');
import '~/lib/utils/common_utils';
/**
* Environments Store.
*
* Stores received environments, count of stopped environments and count of
* available environments.
*/
class EnvironmentsStore {
export default class EnvironmentsStore {
constructor() {
this.state = {};
this.state.environments = [];
......@@ -86,5 +87,3 @@ class EnvironmentsStore {
return count;
}
}
module.exports = EnvironmentsStore;
......@@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => {
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data);
}
Vue.activeResources--;
});
});
......
......@@ -141,6 +141,14 @@
margin-right: 0;
}
}
.no-btn {
border: none;
background: none;
outline: none;
width: 100%;
text-align: left;
}
}
}
......
......@@ -111,10 +111,8 @@ feature 'Environments page', :feature, :js do
find('.js-dropdown-play-icon-container').click
expect(page).to have_content(action.name.humanize)
expect { click_link(action.name.humanize) }
expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
expect(action.reload).to be_pending
end
scenario 'does show build name and id' do
......@@ -158,12 +156,6 @@ feature 'Environments page', :feature, :js do
expect(page).to have_selector('.stop-env-link')
end
scenario 'starts build when stop button clicked' do
find('.stop-env-link').click
expect(page).to have_content('close_app')
end
context 'for reporter' do
let(:role) { :reporter }
......
const ActionsComponent = require('~/environments/components/environment_actions');
import Vue from 'vue';
import actionsComp from '~/environments/components/environment_actions';
describe('Actions Component', () => {
preloadFixtures('static/environments/element.html.raw');
let ActionsComponent;
let actionsMock;
let spy;
let component;
beforeEach(() => {
loadFixtures('static/environments/element.html.raw');
});
ActionsComponent = Vue.extend(actionsComp);
it('should render a dropdown with the provided actions', () => {