diff --git a/zun_ui/api/client.py b/zun_ui/api/client.py index 6a8bb40..eb46ae1 100644 --- a/zun_ui/api/client.py +++ b/zun_ui/api/client.py @@ -152,9 +152,8 @@ def container_update(request, id, **kwargs): return args -def container_delete(request, id, force=False): - # TODO(shu-mutou): force option should be provided by user. - return zunclient(request).containers.delete(id, force=force) +def container_delete(request, **kwargs): + return zunclient(request).containers.delete(**kwargs) def container_list(request, limit=None, marker=None, sort_key=None, diff --git a/zun_ui/api/rest_api.py b/zun_ui/api/rest_api.py index 1d3c073..afe48ec 100644 --- a/zun_ui/api/rest_api.py +++ b/zun_ui/api/rest_api.py @@ -38,14 +38,6 @@ class Container(generic.View): """Get a specific container""" return change_to_id(client.container_show(request, id).to_dict()) - @rest_utils.ajax(data_required=True) - def delete(self, request, id): - """Delete single Container forcely by id. - - Returns HTTP 204 (no content) on successful deletion. - """ - return client.container_delete(request, id, force=True) - @rest_utils.ajax(data_required=True) def patch(self, request, id): """Update a Container. @@ -91,6 +83,19 @@ class ContainerActions(generic.View): elif action == 'attach': return client.container_attach(request, id) + @rest_utils.ajax(data_required=True) + def delete(self, request, id, action): + """Delete specified Container with option. + + Returns HTTP 204 (no content) on successful deletion. + """ + opts = {'id': id} + if action == 'force': + opts['force'] = True + elif action == 'stop': + opts['stop'] = True + return client.container_delete(request, **opts) + @urls.register class Containers(generic.View): @@ -114,7 +119,8 @@ class Containers(generic.View): Returns HTTP 204 (no content) on successful deletion. """ for id in request.DATA: - client.container_delete(request, id) + opts = {'id': id} + client.container_delete(request, **opts) @rest_utils.ajax(data_required=True) def post(self, request): diff --git a/zun_ui/static/dashboard/container/containers/actions.module.js b/zun_ui/static/dashboard/container/containers/actions.module.js index a684961..408cadc 100644 --- a/zun_ui/static/dashboard/container/containers/actions.module.js +++ b/zun_ui/static/dashboard/container/containers/actions.module.js @@ -36,6 +36,7 @@ 'horizon.dashboard.container.containers.update.service', 'horizon.dashboard.container.containers.delete.service', 'horizon.dashboard.container.containers.delete-force.service', + 'horizon.dashboard.container.containers.delete-stop.service', 'horizon.dashboard.container.containers.start.service', 'horizon.dashboard.container.containers.stop.service', 'horizon.dashboard.container.containers.restart.service', @@ -54,6 +55,7 @@ updateContainerService, deleteContainerService, deleteContainerForceService, + deleteContainerStopService, startContainerService, stopContainerService, restartContainerService, @@ -158,6 +160,14 @@ text: gettext('Delete Container') } }) + .append({ + id: 'deleteContainerStopAction', + service: deleteContainerStopService, + template: { + type: 'delete', + text: gettext('Stop and Delete Container') + } + }) .append({ id: 'deleteContainerForceAction', service: deleteContainerForceService, diff --git a/zun_ui/static/dashboard/container/containers/actions/delete-stop.service.js b/zun_ui/static/dashboard/container/containers/actions/delete-stop.service.js new file mode 100644 index 0000000..95ab5d2 --- /dev/null +++ b/zun_ui/static/dashboard/container/containers/actions/delete-stop.service.js @@ -0,0 +1,153 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +(function() { + 'use strict'; + + /** + * @ngDoc factory + * @name horizon.dashboard.container.containers.delete-stop.service + * @Description + * Brings up the stop and delete container confirmation modal dialog. + * On submit, delete after stop selected resources. + * On cancel, do nothing. + */ + angular + .module('horizon.dashboard.container.containers') + .factory('horizon.dashboard.container.containers.delete-stop.service', deleteStopService); + + deleteStopService.$inject = [ + '$location', + '$q', + 'horizon.app.core.openstack-service-api.zun', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.modal.deleteModalService', + 'horizon.framework.widgets.toast.service', + 'horizon.dashboard.container.containers.resourceType', + 'horizon.dashboard.container.containers.events', + 'horizon.dashboard.container.containers.validStates' + ]; + + function deleteStopService( + $location, $q, zun, policy, actionResult, gettext, $qExtensions, deleteModal, + toast, resourceType, events, validStates + ) { + var scope; + var context = { + labels: null, + deleteEntity: deleteEntity, + successEvent: events.DELETE_SUCCESS + }; + var service = { + initAction: initAction, + allowed: allowed, + perform: perform + }; + var notAllowedMessage = gettext("You are not allowed to stop and delete container: %s"); + + return service; + + ////////////// + + function initAction() { + } + + function allowed(container) { + return $qExtensions.booleanAsPromise( + validStates.delete_stop.indexOf(container.status) >= 0 + ); + } + + // delete selected resource objects + function perform(selected, newScope) { + scope = newScope; + selected = angular.isArray(selected) ? selected : [selected]; + context.labels = labelize(selected.length); + return $qExtensions.allSettled(selected.map(checkPermission)).then(afterCheck); + } + + function labelize(count) { + return { + title: ngettext('Confirm Delete After Stop Container', + 'Confirm Delete After Stop Containers', count), + /* eslint-disable max-len */ + message: ngettext('You have selected "%s". Please confirm your selection. The container will be stopped before deleting. Deleted container is not recoverable.', + 'You have selected "%s". Please confirm your selection. The containers will be stopped before deleting. Deleted containers are not recoverable.', count), + /* eslint-enable max-len */ + submit: ngettext('Delete Container After Stop', + 'Delete Containers After Stop', count), + success: ngettext('Deleted Container After Stop: %s.', + 'Deleted Containers After Stop: %s.', count), + error: ngettext('Unable to delete Container after stopping: %s.', + 'Unable to delete Containers after stopping: %s.', count) + }; + } + + // for batch delete + function checkPermission(selected) { + return {promise: allowed(selected), context: selected}; + } + + // for batch delete + function afterCheck(result) { + var outcome = $q.reject(); // Reject the promise by default + if (result.fail.length > 0) { + toast.add('error', getMessage(notAllowedMessage, result.fail)); + outcome = $q.reject(result.fail); + } + if (result.pass.length > 0) { + outcome = deleteModal.open(scope, result.pass.map(getEntity), context).then(createResult); + } + return outcome; + } + + function createResult(deleteModalResult) { + // To make the result of this action generically useful, reformat the return + // from the deleteModal into a standard form + var result = actionResult.getActionResult(); + deleteModalResult.pass.forEach(function markDeleted(item) { + result.deleted(resourceType, getEntity(item).id); + }); + deleteModalResult.fail.forEach(function markFailed(item) { + result.failed(resourceType, getEntity(item).id); + }); + if (result.result.failed.length === 0 && result.result.deleted.length > 0) { + $location.path('/project/container/containers'); + } else { + return result.result; + } + } + + function getMessage(message, entities) { + return interpolate(message, [entities.map(getName).join(", ")]); + } + + function getName(result) { + return getEntity(result).name; + } + + // for batch delete + function getEntity(result) { + return result.context; + } + + // call delete REST API + function deleteEntity(id) { + return zun.deleteContainerStop(id, true); + } + } +})(); diff --git a/zun_ui/static/dashboard/container/containers/containers.module.js b/zun_ui/static/dashboard/container/containers/containers.module.js index cb251a5..9f85918 100644 --- a/zun_ui/static/dashboard/container/containers/containers.module.js +++ b/zun_ui/static/dashboard/container/containers/containers.module.js @@ -67,7 +67,8 @@ delete_force: [ states.CREATED, states.CREATING, states.ERROR, states.RUNNING, states.STOPPED, states.UNKNOWN, states.DELETED - ] + ], + delete_stop: [states.RUNNING] }; } diff --git a/zun_ui/static/dashboard/container/zun.service.js b/zun_ui/static/dashboard/container/zun.service.js index f168ad4..d8ffee5 100644 --- a/zun_ui/static/dashboard/container/zun.service.js +++ b/zun_ui/static/dashboard/container/zun.service.js @@ -35,6 +35,7 @@ deleteContainer: deleteContainer, deleteContainers: deleteContainers, deleteContainerForce: deleteContainerForce, + deleteContainerStop: deleteContainerStop, startContainer: startContainer, stopContainer: stopContainer, logsContainer: logsContainer, @@ -88,13 +89,21 @@ } function deleteContainerForce(id, suppressError) { - var promise = apiService.delete(containersPath + id, [id]); + var promise = apiService.delete(containersPath + id + '/force', [id]); return suppressError ? promise : promise.error(function() { var msg = gettext('Unable to delete forcely the Container with id: %(id)s'); toastService.add('error', interpolate(msg, { id: id }, true)); }); } + function deleteContainerStop(id, suppressError) { + var promise = apiService.delete(containersPath + id + '/stop', [id]); + return suppressError ? promise : promise.error(function() { + var msg = gettext('Unable to stop and delete the Container with id: %(id)s'); + toastService.add('error', interpolate(msg, { id: id }, true)); + }); + } + function startContainer(id) { var msg = gettext('Unable to start Container.'); return apiService.post(containersPath + id + '/start').error(error(msg));