diff --git a/releasenotes/notes/pool-flavor-panel-acf61d32e34246f2.yaml b/releasenotes/notes/pool-flavor-panel-acf61d32e34246f2.yaml new file mode 100644 index 0000000..9f9eb70 --- /dev/null +++ b/releasenotes/notes/pool-flavor-panel-acf61d32e34246f2.yaml @@ -0,0 +1,8 @@ +--- +features: + - > + Storage pool flavors management panel is added. This + panel is added into Admin dashboard. Also create, + update and delete actions are implemeted. Create action + is implemented as globalAction, so it is callable from + other panels. diff --git a/zaqar_ui/enabled/_2530_admin_pool_flavors.py b/zaqar_ui/enabled/_2530_admin_pool_flavors.py index 9754d45..2dd0c18 100644 --- a/zaqar_ui/enabled/_2530_admin_pool_flavors.py +++ b/zaqar_ui/enabled/_2530_admin_pool_flavors.py @@ -18,5 +18,6 @@ PANEL_DASHBOARD = 'admin' ADD_PANEL = ('zaqar_ui.content.pool-flavors.panel.PoolFlavors') ADD_ANGULAR_MODULES = ['horizon.dashboard.admin.pool-flavors'] +ADD_SCSS_FILES = ['dashboard/admin/pool-flavors/pool-flavors.scss'] AUTO_DISCOVER_STATIC_FILES = True diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/actions/actions.module.js b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/actions.module.js new file mode 100644 index 0000000..628a57e --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/actions.module.js @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this 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 overview + * @ngname horizon.dashboard.admin.pool-flavors.actions + * + * @description + * Provides all of the actions for pool flavors. + */ + angular.module('horizon.dashboard.admin.pool-flavors.actions', [ + 'horizon.framework.conf', + 'horizon.dashboard.admin.pool-flavors' + ]) + .run(registerPoolFlavorActions); + + registerPoolFlavorActions.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.dashboard.admin.pool-flavors.actions.create.service', + 'horizon.dashboard.admin.pool-flavors.actions.delete.service', + 'horizon.dashboard.admin.pool-flavors.actions.update.service', + 'horizon.dashboard.admin.pool-flavors.resourceType' + ]; + + function registerPoolFlavorActions( + registry, + createPoolFlavorService, + deletePoolFlavorService, + updatePoolFlavorService, + flavorResourceType + ) { + var resourceType = registry.getResourceType(flavorResourceType); + + resourceType.globalActions + .append({ + id: 'createPoolFlavorAction', + service: createPoolFlavorService, + template: { + text: gettext('Create Pool Flavor'), + type: 'create' + } + }); + + resourceType.batchActions + .append({ + id: 'batchDeletePoolFlavorAction', + service: deletePoolFlavorService, + template: { + type: 'delete-selected', + text: gettext('Delete Pool Flavors') + } + }); + + resourceType.itemActions + .append({ + id: 'updatePoolFlavorAction', + service: updatePoolFlavorService, + template: { + text: gettext('Update Pool Flavor'), + type: 'row' + } + }) + .append({ + id: 'deletePoolFlavorAction', + service: deletePoolFlavorService, + template: { + text: gettext('Delete Pool Flavor'), + type: 'delete' + } + }); + } +})(); diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/actions/create.service.js b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/create.service.js new file mode 100644 index 0000000..344e3ad --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/create.service.js @@ -0,0 +1,84 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this 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.admin.pool-flavors.actions.create.service + * @description + * Service for the pool flavor create modal + */ + angular + .module('horizon.dashboard.admin.pool-flavors.actions') + .factory('horizon.dashboard.admin.pool-flavors.actions.create.service', createService); + + createService.$inject = [ + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.zaqar', + 'horizon.dashboard.admin.pool-flavors.actions.workflow', + 'horizon.dashboard.admin.pool-flavors.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + function createService( + policy, zaqar, workflow, resourceType, + actionResult, gettext, $qExtensions, modal, toast + ) { + + var message = { + success: gettext('Pool flavor %s was successfully created.') + }; + + var service = { + initAction: initAction, + perform: perform, + allowed: allowed + }; + + return service; + + ////////////// + + function initAction() { + } + + function perform() { + var title, submitText; + title = gettext('Create Pool Flavor'); + submitText = gettext('Create'); + var config = workflow.init('create', title, submitText); + return modal.open(config).then(submit); + } + + function allowed() { + return policy.ifAllowed({ rules: [['pool_flavor', 'add_flavor']] }); + } + + function submit(context) { + return zaqar.createFlavor(context.model, true).then(success, true); + } + + function success(response) { + toast.add('success', interpolate(message.success, [response.data.id])); + var result = actionResult.getActionResult().created(resourceType, response.data.name); + return result.result; + } + } +})(); diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/actions/delete.service.js b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/delete.service.js new file mode 100644 index 0000000..952916d --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/delete.service.js @@ -0,0 +1,138 @@ +/** + * 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.admin.pool-flavors.actions.delete.service + * @Description + * Brings up the delete pool flavors confirmation modal dialog. + * On submit, delete given pool flavors. + * On cancel, do nothing. + */ + angular + .module('horizon.dashboard.admin.pool-flavors.actions') + .factory('horizon.dashboard.admin.pool-flavors.actions.delete.service', deleteService); + + deleteService.$inject = [ + '$q', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.zaqar', + 'horizon.dashboard.admin.pool-flavors.resourceType', + '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' + ]; + + function deleteService( + $q, policy, zaqar, resourceType, actionResult, gettext, $qExtensions, + deleteModal, toast + ) { + var scope, context; + var notAllowedMessage = gettext("You are not allowed to delete pool flavors: %s"); + + var service = { + initAction: initAction, + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + function initAction() { + context = { }; + } + + function perform(items, newScope) { + scope = newScope; + var flavors = angular.isArray(items) ? items : [items]; + context.labels = labelize(flavors.length); + context.deleteEntity = deleteFlavor; + return $qExtensions.allSettled(flavors.map(checkPermission)).then(afterCheck); + } + + function allowed() { + return policy.ifAllowed({ rules: [['pool_flavor', 'delete_flavor']] }); + } + + function checkPermission(flavor) { + return {promise: allowed(), context: flavor}; + } + + 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) { + var result = actionResult.getActionResult(); + deleteModalResult.pass.forEach(function markDeleted(item) { + result.deleted(resourceType, getEntity(item).name); + }); + deleteModalResult.fail.forEach(function markFailed(item) { + result.failed(resourceType, getEntity(item).name); + }); + return result.result; + } + + function labelize(count) { + return { + title: ngettext( + 'Confirm Delete Pool Flavor', + 'Confirm Delete Pool Flavors', count), + message: ngettext( + 'You have selected "%s". Deleted Pool Flavor is not recoverable.', + 'You have selected "%s". Deleted Pool Flavors are not recoverable.', count), + submit: ngettext( + 'Delete Pool Flavor', + 'Delete Pool Flavors', count), + success: ngettext( + 'Deleted Pool Flavor: %s.', + 'Deleted Pool Flavors: %s.', count), + error: ngettext( + 'Unable to delete Pool Flavor: %s.', + 'Unable to delete Pool Flavors: %s.', count) + }; + } + + function deleteFlavor(flavor) { + return zaqar.deleteFlavor(flavor, true); + } + + function getMessage(message, entities) { + return interpolate(message, [entities.map(getName).join(", ")]); + } + + function getName(result) { + return getEntity(result).name; + } + + function getEntity(result) { + return result.context; + } + } +})(); diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/actions/update.service.js b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/update.service.js new file mode 100644 index 0000000..86daa87 --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/update.service.js @@ -0,0 +1,93 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this 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.admin.pool-flavors.actions.update.service + * @description + * Service for the pool flavor update modal + */ + angular + .module('horizon.dashboard.admin.pool-flavors.actions') + .factory('horizon.dashboard.admin.pool-flavors.actions.update.service', updateService); + + updateService.$inject = [ + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.zaqar', + 'horizon.dashboard.admin.pool-flavors.actions.workflow', + 'horizon.dashboard.admin.pool-flavors.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + function updateService( + policy, zaqar, workflow, resourceType, + actionResult, gettext, $qExtensions, modal, toast + ) { + + var message = { + success: gettext('Pool flavor %s was successfully updated.') + }; + + var service = { + initAction: initAction, + perform: perform, + allowed: allowed + }; + + return service; + + ////////////// + + function initAction() { + } + + function perform(selected) { + var title, submitText; + title = gettext('Update Pool Flavor'); + submitText = gettext('Update'); + var config = workflow.init('update', title, submitText); + + // load current data + zaqar.getFlavor(selected.name).then(onLoad); + function onLoad(response) { + config.model.name = response.data.name; + config.model.pool_group = response.data.pool_group; + config.model.capabilities = response.data.capabilities; + } + + return modal.open(config).then(submit); + } + + function allowed() { + return policy.ifAllowed({ rules: [['pool_flavor', 'update_flavor']] }); + } + + function submit(context) { + return zaqar.updateFlavor(context.model, true).then(success, true); + } + + function success(response) { + toast.add('success', interpolate(message.success, [response.data.name])); + var result = actionResult.getActionResult().updated(resourceType, response.data.name); + return result.result; + } + } +})(); diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/actions/workflow.service.js b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/workflow.service.js new file mode 100644 index 0000000..d55fedb --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/actions/workflow.service.js @@ -0,0 +1,120 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this 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.admin.pool-flavors.actions.workflow + * @description + * Workflow for creating/updating storage pool flavor + */ + angular + .module('horizon.dashboard.admin.pool-flavors.actions') + .factory('horizon.dashboard.admin.pool-flavors.actions.workflow', workflow); + + workflow.$inject = [ + 'horizon.framework.util.i18n.gettext' + ]; + + function workflow(gettext) { + var workflow = { + init: init + }; + + function init(actionType, title, submitText) { + var schema, form, model; + var capabilitiesPlaceholder = gettext( + 'Describes flavor-specific capabilities in YAML format.'); + + // schema + schema = { + type: 'object', + properties: { + name: { + title: gettext('Name'), + type: 'string' + }, + pool_group: { + title: gettext('Pool Group'), + type: 'string' + }, + capabilities: { + title: gettext('Capabilities'), + type: 'string' + } + } + }; + + // form + form = [ + { + type: 'section', + htmlClass: 'row', + items: [ + { + type: 'section', + htmlClass: 'col-sm-6', + items: [ + { + key: 'name', + placeholder: gettext('Name of the flavor.'), + required: true, + "readonly": actionType === 'update' + }, + { + key: 'pool_group', + placeholder: gettext('Pool group for flavor.'), + /* eslint-disable max-len */ + description: gettext('You must specify one of the pool groups that is configured in storage pools.'), + required: true + } + ] + }, + { + type: 'section', + htmlClass: 'col-sm-6', + items: [ + { + key: 'capabilities', + type: 'textarea', + placeholder: capabilitiesPlaceholder + } + ] + } + ] + } + ]; // form + + model = { + name: '', + pool_group: '', + capabilities: '' + }; + + var config = { + title: title, + submitText: submitText, + schema: schema, + form: form, + model: model + }; + + return config; + } + + return workflow; + } +})(); diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/panel.html b/zaqar_ui/static/dashboard/admin/pool-flavors/panel.html index a742faa..fd645c9 100644 --- a/zaqar_ui/static/dashboard/admin/pool-flavors/panel.html +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/panel.html @@ -1,4 +1,4 @@ - + diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.module.js b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.module.js index 472215e..69f852b 100644 --- a/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.module.js +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.module.js @@ -22,7 +22,8 @@ angular .module('horizon.dashboard.admin.pool-flavors', [ - 'ngRoute' + 'ngRoute', + 'horizon.dashboard.admin.pool-flavors.actions' ]) .constant('horizon.dashboard.admin.pool-flavors.resourceType', 'OS::Zaqar::Flavors') .run(run) diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.scss b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.scss new file mode 100644 index 0000000..917c10d --- /dev/null +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.scss @@ -0,0 +1,4 @@ + +textarea#capabilities { + height: 10em; +} \ No newline at end of file diff --git a/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.service.js b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.service.js index 0450e33..a245b9a 100644 --- a/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.service.js +++ b/zaqar_ui/static/dashboard/admin/pool-flavors/pool-flavors.service.js @@ -43,7 +43,19 @@ * flavors. This is used in displaying lists of Pool Flavors. */ function getFlavorsPromise(params) { - return zaqar.getFlavors(params); + return zaqar.getFlavors(params).then(modifyResponse); + } + + function modifyResponse(response) { + return {data: {items: response.data.items.map(modifyItem)}}; + + function modifyItem(item) { + // we should set 'trackBy' as follows ideally. + // item.trackBy = item.id + item.updated_at; + var timestamp = new Date(); + item.trackBy = item.name.concat(timestamp.getTime()); + return item; + } } } })();