Add pool delete action
Adds the pool delete action to the pool details page. Partially-Implements: blueprint horizon-lbaas-v2-ui Change-Id: Ic3ffa9eca363ce73b20841775c3dce1612fafb07
This commit is contained in:
parent
48ae48f04f
commit
a7f1b762ed
|
@ -576,6 +576,14 @@ class Pool(generic.View):
|
|||
lb = neutronclient(request).show_lbaas_pool(pool_id)
|
||||
return lb.get('pool')
|
||||
|
||||
@rest_utils.ajax()
|
||||
def delete(self, request, pool_id):
|
||||
"""Delete a specific pool.
|
||||
|
||||
http://localhost/api/lbaas/pools/cc758c90-3d98-4ea1-af44-aab405c9c915
|
||||
"""
|
||||
neutronclient(request).delete_lbaas_pool(pool_id)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Members(generic.View):
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
editListener: editListener,
|
||||
deleteListener: deleteListener,
|
||||
getPool: getPool,
|
||||
deletePool: deletePool,
|
||||
getMembers: getMembers,
|
||||
getMember: getMember,
|
||||
getHealthMonitor: getHealthMonitor
|
||||
|
@ -248,6 +249,22 @@
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.lbaasv2.deletePool
|
||||
* @description
|
||||
* Delete a single pool by ID
|
||||
* @param {string} id
|
||||
* @param {boolean} quiet
|
||||
* Specifies the id of the pool to delete.
|
||||
*/
|
||||
|
||||
function deletePool(id, quiet) {
|
||||
var promise = apiService.delete('/api/lbaas/pools/' + id);
|
||||
return quiet ? promise : promise.error(function () {
|
||||
toastService.add('error', gettext('Unable to delete pool.'));
|
||||
});
|
||||
}
|
||||
|
||||
// Members
|
||||
|
||||
/**
|
||||
|
|
|
@ -97,6 +97,13 @@
|
|||
error: 'Unable to retrieve pool.',
|
||||
testInput: [ '1234' ]
|
||||
},
|
||||
{
|
||||
func: 'deletePool',
|
||||
method: 'delete',
|
||||
path: '/api/lbaas/pools/1234',
|
||||
error: 'Unable to delete pool.',
|
||||
testInput: [ '1234' ]
|
||||
},
|
||||
{
|
||||
func: 'getMembers',
|
||||
method: 'get',
|
||||
|
@ -177,6 +184,11 @@
|
|||
expect(service.deleteListener("whatever", true)).toBe("promise");
|
||||
});
|
||||
|
||||
it('supresses the error if instructed for deletePool', function() {
|
||||
spyOn(apiService, 'delete').and.returnValue("promise");
|
||||
expect(service.deletePool("whatever", true)).toBe("promise");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
$q = $injector.get('$q');
|
||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||
service = $injector.get('horizon.dashboard.project.lbaasv2.listeners.actions.delete');
|
||||
service.init('1');
|
||||
service.init('1', makePromise());
|
||||
}));
|
||||
|
||||
it('should have the "allowed" and "perform" functions', function() {
|
||||
|
@ -96,6 +96,11 @@
|
|||
expect(allowed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow deleting listener from load balancer in a PENDING state', function() {
|
||||
service.init('1', makePromise(true));
|
||||
expect(allowed()).toBe(false);
|
||||
});
|
||||
|
||||
it('should open the delete modal', function() {
|
||||
spyOn(modal, 'open');
|
||||
service.perform(items[0]);
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2016 IBM Corp.
|
||||
*
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.lbaasv2.pools')
|
||||
.factory('horizon.dashboard.project.lbaasv2.pools.actions.delete', deleteService);
|
||||
|
||||
deleteService.$inject = [
|
||||
'$q',
|
||||
'$location',
|
||||
'$route',
|
||||
'horizon.framework.widgets.modal.deleteModalService',
|
||||
'horizon.app.core.openstack-service-api.lbaasv2',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngDoc factory
|
||||
* @name horizon.dashboard.project.lbaasv2.pools.actions.deleteService
|
||||
* @description
|
||||
* Brings up the delete pool confirmation modal dialog.
|
||||
* On submit, deletes selected pool.
|
||||
* On cancel, does nothing.
|
||||
* @param $q The angular service for promises.
|
||||
* @param $location The angular $location service.
|
||||
* @param $route The angular $route service.
|
||||
* @param deleteModal The horizon delete modal service.
|
||||
* @param api The LBaaS v2 API service.
|
||||
* @param policy The horizon policy service.
|
||||
* @param gettext The horizon gettext function for translation.
|
||||
* @returns The load balancers table delete service.
|
||||
*/
|
||||
|
||||
function deleteService($q, $location, $route, deleteModal, api, policy, gettext) {
|
||||
var loadbalancerId, listenerId, statePromise;
|
||||
var context = {
|
||||
labels: {
|
||||
title: gettext('Confirm Delete Pool'),
|
||||
message: gettext('You have selected "%s". Please confirm your selection. Deleted pools ' +
|
||||
'are not recoverable.'),
|
||||
submit: gettext('Delete Pool'),
|
||||
success: gettext('Deleted pool: %s.'),
|
||||
error: gettext('The following pool could not be deleted: %s.')
|
||||
},
|
||||
deleteEntity: deleteItem,
|
||||
successEvent: 'success',
|
||||
failedEvent: 'error'
|
||||
};
|
||||
|
||||
var service = {
|
||||
perform: perform,
|
||||
allowed: allowed,
|
||||
init: init
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
function init(_loadbalancerId_, _listenerId_, _statePromise_) {
|
||||
loadbalancerId = _loadbalancerId_;
|
||||
listenerId = _listenerId_;
|
||||
statePromise = _statePromise_;
|
||||
return service;
|
||||
}
|
||||
|
||||
function perform(item) {
|
||||
deleteModal.open({ $emit: actionComplete }, [item], context);
|
||||
}
|
||||
|
||||
function allowed(/*item*/) {
|
||||
return $q.all([
|
||||
statePromise,
|
||||
// This rule is made up and should therefore always pass. I assume at some point there
|
||||
// will be a valid rule similar to this that we will want to use.
|
||||
policy.ifAllowed({ rules: [['neutron', 'delete_pool']] })
|
||||
]);
|
||||
}
|
||||
|
||||
function deleteItem(id) {
|
||||
return api.deletePool(id, true);
|
||||
}
|
||||
|
||||
function actionComplete(eventType) {
|
||||
if (eventType === context.failedEvent) {
|
||||
// Error, reload page
|
||||
$route.reload();
|
||||
} else {
|
||||
// Success, go back to listener details page
|
||||
var path = 'project/ngloadbalancersv2/' + loadbalancerId + '/listeners/' + listenerId;
|
||||
$location.path(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright 2016 IBM Corp.
|
||||
*
|
||||
* 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';
|
||||
|
||||
describe('LBaaS v2 Pool Delete Service', function() {
|
||||
var service, policy, modal, lbaasv2Api, $scope, $location, $q, toast, pool, path;
|
||||
|
||||
function allowed(item) {
|
||||
spyOn(policy, 'ifAllowed').and.returnValue(makePromise());
|
||||
var promise = service.allowed(item);
|
||||
var allowed;
|
||||
promise.then(function() {
|
||||
allowed = true;
|
||||
}, function() {
|
||||
allowed = false;
|
||||
});
|
||||
$scope.$apply();
|
||||
expect(policy.ifAllowed).toHaveBeenCalledWith({rules: [['neutron', 'delete_pool']]});
|
||||
return allowed;
|
||||
}
|
||||
|
||||
function makePromise(reject) {
|
||||
var def = $q.defer();
|
||||
def[reject ? 'reject' : 'resolve']();
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
function isActionable(id) {
|
||||
if (id === 'active') {
|
||||
return $q.when();
|
||||
} else {
|
||||
return $q.reject();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||
|
||||
beforeEach(function() {
|
||||
pool = { id: '1', name: 'Pool1' };
|
||||
});
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('$modal', {
|
||||
open: function() {
|
||||
return {
|
||||
result: makePromise()
|
||||
};
|
||||
}
|
||||
});
|
||||
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
|
||||
deletePool: function() {
|
||||
return makePromise();
|
||||
}
|
||||
});
|
||||
$provide.value('$location', {
|
||||
path: function() {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
|
||||
lbaasv2Api = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
|
||||
modal = $injector.get('horizon.framework.widgets.modal.deleteModalService');
|
||||
$scope = $injector.get('$rootScope').$new();
|
||||
$location = $injector.get('$location');
|
||||
$q = $injector.get('$q');
|
||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||
service = $injector.get('horizon.dashboard.project.lbaasv2.pools.actions.delete');
|
||||
service.init('1', '2', isActionable('active'));
|
||||
$scope.$apply();
|
||||
}));
|
||||
|
||||
it('should have the "allowed" and "perform" functions', function() {
|
||||
expect(service.allowed).toBeDefined();
|
||||
expect(service.perform).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow deleting pool from load balancer in ACTIVE state', function() {
|
||||
expect(allowed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow deleting pool from load balancer in a PENDING state', function() {
|
||||
service.init('1', '2', isActionable('pending'));
|
||||
expect(allowed()).toBe(false);
|
||||
});
|
||||
|
||||
it('should open the delete modal', function() {
|
||||
spyOn(modal, 'open');
|
||||
service.perform(pool);
|
||||
$scope.$apply();
|
||||
expect(modal.open.calls.count()).toBe(1);
|
||||
var args = modal.open.calls.argsFor(0);
|
||||
expect(args.length).toBe(3);
|
||||
expect(args[0]).toEqual({ $emit: jasmine.any(Function) });
|
||||
expect(args[1]).toEqual([pool]);
|
||||
expect(args[2]).toEqual(jasmine.objectContaining({
|
||||
labels: jasmine.any(Object),
|
||||
deleteEntity: jasmine.any(Function)
|
||||
}));
|
||||
expect(args[2].labels.title).toBe('Confirm Delete Pool');
|
||||
});
|
||||
|
||||
it('should pass function to modal that deletes the pool', function() {
|
||||
spyOn(modal, 'open').and.callThrough();
|
||||
spyOn(lbaasv2Api, 'deletePool').and.callThrough();
|
||||
service.perform(pool);
|
||||
$scope.$apply();
|
||||
expect(lbaasv2Api.deletePool.calls.count()).toBe(1);
|
||||
expect(lbaasv2Api.deletePool).toHaveBeenCalledWith('1', true);
|
||||
});
|
||||
|
||||
it('should show message if any items fail to be deleted', function() {
|
||||
spyOn(modal, 'open').and.callThrough();
|
||||
spyOn(lbaasv2Api, 'deletePool').and.returnValue(makePromise(true));
|
||||
spyOn(toast, 'add');
|
||||
service.perform(pool);
|
||||
$scope.$apply();
|
||||
expect(modal.open).toHaveBeenCalled();
|
||||
expect(lbaasv2Api.deletePool.calls.count()).toBe(1);
|
||||
expect(toast.add).toHaveBeenCalledWith('error', 'The following pool could not ' +
|
||||
'be deleted: Pool1.');
|
||||
});
|
||||
|
||||
it('should return to listener details after delete', function() {
|
||||
path = 'project/ngloadbalancersv2/1';
|
||||
spyOn($location, 'path');
|
||||
spyOn(toast, 'add');
|
||||
service.perform(pool);
|
||||
$scope.$apply();
|
||||
expect($location.path).toHaveBeenCalledWith('project/ngloadbalancersv2/1/listeners/2');
|
||||
expect(toast.add).toHaveBeenCalledWith('success', 'Deleted pool: Pool1.');
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2016 IBM Corp.
|
||||
*
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.lbaasv2.pools')
|
||||
.factory('horizon.dashboard.project.lbaasv2.pools.actions.rowActions',
|
||||
rowActions);
|
||||
|
||||
rowActions.$inject = [
|
||||
'horizon.framework.util.i18n.gettext',
|
||||
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
|
||||
'horizon.dashboard.project.lbaasv2.pools.actions.delete'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @ngname horizon.dashboard.project.lbaasv2.pools.actions.rowActions
|
||||
*
|
||||
* @description
|
||||
* Provides the service for the pool row actions.
|
||||
*
|
||||
* @param gettext The horizon gettext function for translation.
|
||||
* @param loadBalancersService The LBaaS v2 load balancers service.
|
||||
* @param deleteService The LBaaS v2 pools delete service.
|
||||
* @returns Pool row actions service object.
|
||||
*/
|
||||
|
||||
function rowActions(gettext, loadBalancersService, deleteService) {
|
||||
var loadBalancerIsActionable, loadbalancerId, listenerId;
|
||||
|
||||
var service = {
|
||||
actions: actions,
|
||||
init: init
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
///////////////
|
||||
|
||||
function init(_loadbalancerId_, _listenerId_) {
|
||||
loadbalancerId = _loadbalancerId_;
|
||||
listenerId = _listenerId_;
|
||||
loadBalancerIsActionable = loadBalancersService.isActionable(loadbalancerId);
|
||||
return service;
|
||||
}
|
||||
|
||||
function actions() {
|
||||
return [{
|
||||
service: deleteService.init(loadbalancerId, listenerId, loadBalancerIsActionable),
|
||||
template: {
|
||||
text: gettext('Delete Pool'),
|
||||
type: 'delete'
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2016 IBM Corp.
|
||||
*
|
||||
* 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';
|
||||
|
||||
describe('LBaaS v2 Pools Row Actions Service', function() {
|
||||
var actions;
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
var rowActionsService = $injector.get(
|
||||
'horizon.dashboard.project.lbaasv2.pools.actions.rowActions');
|
||||
actions = rowActionsService.init('1', '2').actions();
|
||||
var loadbalancerService = $injector.get(
|
||||
'horizon.dashboard.project.lbaasv2.loadbalancers.service');
|
||||
spyOn(loadbalancerService, 'isActionable').and.returnValue(true);
|
||||
}));
|
||||
|
||||
it('should define correct table row actions', function() {
|
||||
expect(actions.length).toBe(1);
|
||||
expect(actions[0].template.text).toBe('Delete Pool');
|
||||
});
|
||||
|
||||
it('should have the "allowed" and "perform" functions', function() {
|
||||
actions.forEach(function(action) {
|
||||
expect(action.service.allowed).toBeDefined();
|
||||
expect(action.service.perform).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
PoolDetailController.$inject = [
|
||||
'horizon.app.core.openstack-service-api.lbaasv2',
|
||||
'horizon.dashboard.project.lbaasv2.pools.actions.rowActions',
|
||||
'$routeParams',
|
||||
'horizon.framework.util.i18n.gettext'
|
||||
];
|
||||
|
@ -34,12 +35,13 @@
|
|||
* Controller for the LBaaS v2 pool detail page.
|
||||
*
|
||||
* @param api The LBaaS v2 API service.
|
||||
* @param rowActions The LBaaS v2 pool row actions service.
|
||||
* @param $routeParams The angular $routeParams service.
|
||||
* @param gettext The horizon gettext function for translation.
|
||||
* @returns undefined
|
||||
*/
|
||||
|
||||
function PoolDetailController(api, $routeParams, gettext) {
|
||||
function PoolDetailController(api, rowActions, $routeParams, gettext) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.loadBalancerAlgorithm = {
|
||||
|
@ -48,6 +50,8 @@
|
|||
'SOURCE_IP': gettext('Source IP')
|
||||
};
|
||||
|
||||
ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.listenerId).actions;
|
||||
|
||||
init();
|
||||
|
||||
////////////////////////////////
|
||||
|
|
|
@ -27,10 +27,22 @@
|
|||
};
|
||||
}
|
||||
|
||||
function loadbalancerAPI() {
|
||||
var loadbalancer = { provisioning_status: 'ACTIVE' };
|
||||
return {
|
||||
success: function(callback) {
|
||||
callback(loadbalancer);
|
||||
},
|
||||
then: function(callback) {
|
||||
callback({ data: loadbalancer });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
beforeEach(module('horizon.framework.widgets.toast'));
|
||||
beforeEach(module('horizon.framework.widgets'));
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||
|
@ -39,7 +51,7 @@
|
|||
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
|
||||
spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI);
|
||||
spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI);
|
||||
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeAPI);
|
||||
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
|
||||
var controller = $injector.get('$controller');
|
||||
ctrl = controller('PoolDetailController', {
|
||||
$routeParams: {
|
||||
|
@ -54,7 +66,7 @@
|
|||
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
|
||||
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
|
||||
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
|
||||
expect(ctrl.loadbalancer).toBe('foo');
|
||||
expect(ctrl.loadbalancer).toEqual({provisioning_status: 'ACTIVE'});
|
||||
expect(ctrl.listener).toBe('foo');
|
||||
expect(ctrl.pool).toBe('foo');
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
|
||||
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li>
|
||||
<li class="active">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</li>
|
||||
<actions allowed="ctrl.actions" type="row" item="ctrl.pool"
|
||||
ng-if="ctrl.pool" class="actions_column pull-right"></actions>
|
||||
</ol>
|
||||
<p ng-if="::ctrl.pool.description">{$ ::ctrl.pool.description $}</p>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue