Merge "Support post messages and get messages"
This commit is contained in:
commit
174f4e24ff
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import six
|
||||
import yaml
|
||||
|
||||
@ -138,7 +139,7 @@ class Queues(generic.View):
|
||||
|
||||
@urls.register
|
||||
class Subscriptions(generic.View):
|
||||
"""API for queues"""
|
||||
"""API for Subscriptions"""
|
||||
url_regex = r'zaqar/queues/(?P<queue_name>[^/]+)/subscriptions/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
@ -163,6 +164,35 @@ class Subscriptions(generic.View):
|
||||
return zaqar.subscription_create(request, queue_name, request.DATA)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Messages(generic.View):
|
||||
"""API for messages"""
|
||||
url_regex = r'zaqar/queues/(?P<queue_name>[^/]+)/messages/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, queue_name):
|
||||
"""Get a list of messages"""
|
||||
result = zaqar.message_list(request, queue_name)
|
||||
messages = []
|
||||
for m in result:
|
||||
claim_id = None
|
||||
if m.claim_id:
|
||||
claim_id = m.claim_id()
|
||||
messages.append({'age': m.age,
|
||||
'body': m.body,
|
||||
'claim_id': claim_id,
|
||||
'id': m.id,
|
||||
'href': m.href,
|
||||
'ttl': m.ttl})
|
||||
return messages
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def post(self, request, queue_name):
|
||||
"""Create new messages"""
|
||||
messages = json.loads(request.DATA.get("messages"))
|
||||
return zaqar.message_post(request, queue_name, messages)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Subscription(generic.View):
|
||||
"""API for retrieving a single subscription"""
|
||||
|
@ -94,6 +94,14 @@ def queue_purge(request, queue_name, resource_types):
|
||||
queue.purge(resource_types=resource_types)
|
||||
|
||||
|
||||
def message_post(request, queue_name, messages_data):
|
||||
return zaqarclient(request).queue(queue_name).post(messages_data)
|
||||
|
||||
|
||||
def message_list(request, queue_name):
|
||||
return zaqarclient(request).queue(queue_name).messages()
|
||||
|
||||
|
||||
def subscription_list(request, queue_name):
|
||||
return [{'subscriber': s.subscriber,
|
||||
'id': s.id,
|
||||
|
@ -65,6 +65,7 @@ module.exports = function (config) {
|
||||
toxPath + 'xstatic/pkg/tv4/data/tv4.js',
|
||||
toxPath + 'xstatic/pkg/objectpath/data/ObjectPath.js',
|
||||
toxPath + 'xstatic/pkg/angular_schema_form/data/schema-form.js',
|
||||
toxPath + 'xstatic/pkg/angular_fileupload/data/ng-file-upload.js',
|
||||
|
||||
// TODO: These should be mocked.
|
||||
toxPath + '/horizon/static/horizon/js/horizon.js',
|
||||
|
@ -29,6 +29,7 @@
|
||||
function ZaqarAPI(apiService, toast) {
|
||||
|
||||
var queuePath = '/api/zaqar/queues/';
|
||||
var msgPath = '/api/zaqar/queues/%s/messages/';
|
||||
var subPath = '/api/zaqar/queues/%s/subscriptions/';
|
||||
var poolPath = '/api/zaqar/pools/';
|
||||
var flavorPath = '/api/zaqar/flavors/';
|
||||
@ -40,6 +41,8 @@
|
||||
deleteQueue: deleteQueue,
|
||||
updateQueue: updateQueue,
|
||||
purgeQueue: purgeQueue,
|
||||
postMessages: postMessages,
|
||||
getMessages: getMessages,
|
||||
getSubscriptions: getSubscriptions,
|
||||
addSubscription: addSubscription,
|
||||
deleteSubscription: deleteSubscription,
|
||||
@ -92,6 +95,17 @@
|
||||
return apiService.post(url, form).error(error(msg));
|
||||
}
|
||||
|
||||
function getMessages(queueName) {
|
||||
var url = interpolate(msgPath, [queueName]);
|
||||
return apiService.get(url);
|
||||
}
|
||||
|
||||
function postMessages(queueName, msgs) {
|
||||
var msg = gettext('Unable to post messages.');
|
||||
var url = interpolate(msgPath, [queueName]);
|
||||
return apiService.post(url, msgs).error(error(msg));
|
||||
}
|
||||
|
||||
function getSubscriptions(queue) {
|
||||
var url = interpolate(subPath, [queue.name]);
|
||||
return apiService.get(url);
|
||||
|
@ -33,6 +33,8 @@
|
||||
'horizon.dashboard.project.queues.actions.deleteQueueService',
|
||||
'horizon.dashboard.project.queues.actions.updateQueueService',
|
||||
'horizon.dashboard.project.queues.actions.purgeQueueService',
|
||||
'horizon.dashboard.project.queues.actions.postMessageService',
|
||||
'horizon.dashboard.project.queues.actions.listMessageService',
|
||||
'horizon.dashboard.project.queues.actions.createSubscriptionService',
|
||||
'horizon.dashboard.project.queues.resourceType'
|
||||
];
|
||||
@ -43,12 +45,28 @@
|
||||
deleteQueueService,
|
||||
updateQueueService,
|
||||
purgeQueueService,
|
||||
postMessageService,
|
||||
listMessageService,
|
||||
createSubscriptionService,
|
||||
resourceType
|
||||
) {
|
||||
|
||||
var queueResourceType = registry.getResourceType(resourceType);
|
||||
queueResourceType.itemActions
|
||||
.append({
|
||||
id: 'messagesPost',
|
||||
service: postMessageService,
|
||||
template: {
|
||||
text: gettext('Post Messages')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'messagesList',
|
||||
service: listMessageService,
|
||||
template: {
|
||||
text: gettext('View Messages')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'queuesItemUpdate',
|
||||
service: updateQueueService,
|
||||
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright 2017 Catalyst IT Ltd.
|
||||
*
|
||||
* 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
|
||||
* @name messageController
|
||||
* @ngController
|
||||
*
|
||||
* @description
|
||||
* Controller for the messages table
|
||||
*/
|
||||
angular
|
||||
.module('horizon.dashboard.project.queues.actions')
|
||||
.controller('horizon.dashboard.project.queues.actions.messageController',
|
||||
messageController);
|
||||
|
||||
messageController.$inject = [
|
||||
'$scope',
|
||||
'horizon.app.core.openstack-service-api.zaqar'
|
||||
];
|
||||
|
||||
function messageController($scope, zaqar) {
|
||||
|
||||
var ctrl = this;
|
||||
ctrl.queue = $scope.model.id;
|
||||
ctrl.messages = [];
|
||||
ctrl.claimMessage = claimMessage;
|
||||
ctrl.deleteMessage = deleteMessage;
|
||||
|
||||
zaqar.getMessages(ctrl.queue).success(function (response) {
|
||||
ctrl.messages = response;
|
||||
});
|
||||
|
||||
//////////
|
||||
|
||||
/* TODO: actions will be implemented later.
|
||||
function claimMessage(message) {
|
||||
console.info(message);
|
||||
}
|
||||
|
||||
function deleteMessage(message) {
|
||||
console.info(message);
|
||||
}
|
||||
*/
|
||||
}
|
||||
})();
|
@ -0,0 +1,33 @@
|
||||
<table class="table tabler-inner"
|
||||
ng-controller="horizon.dashboard.project.queues.actions.messageController as msgCtrl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>ID</th>
|
||||
<th translate>Body</th>
|
||||
<th translate>Time to Live</th>
|
||||
<th translate>Age</th>
|
||||
<!--
|
||||
<th></th>
|
||||
-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="msgCtrl.messages.length === 0">
|
||||
<td colspan="100">No message to show.</td>
|
||||
</tr>
|
||||
<tr ng-repeat="msg in msgCtrl.messages">
|
||||
<td>{$ msg.id $}</td>
|
||||
<td>{$ msg.body $}</td>
|
||||
<td>{$ msg.ttl $}</td>
|
||||
<td>{$ msg.age $}</td>
|
||||
<!--
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger"
|
||||
ng-click="msgCtrl.deleteMessage(msg)">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</td>
|
||||
-->
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.queues')
|
||||
.factory(
|
||||
'horizon.dashboard.project.queues.actions.listMessageService', listMessageService);
|
||||
|
||||
listMessageService.$inject = [
|
||||
'$q',
|
||||
'horizon.dashboard.project.queues.basePath',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.app.core.openstack-service-api.zaqar',
|
||||
'horizon.dashboard.project.queues.events',
|
||||
'horizon.framework.util.i18n.gettext',
|
||||
'horizon.framework.util.q.extensions',
|
||||
'horizon.framework.widgets.form.ModalFormService',
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.dashboard.project.queues.actions.listMessageService
|
||||
* @param {Object} $q
|
||||
* @param {String} basePath
|
||||
* @param {Object} policy
|
||||
* @param {Object} zaqar
|
||||
* @param {Object} events
|
||||
* @param {Object} gettext
|
||||
* @param {Object} $qExtensions
|
||||
* @param {Object} modal
|
||||
* @param {Object} toast
|
||||
* @returns {Object} list messages service
|
||||
* @description Brings up the polling messages modal dialog.
|
||||
* On submit, poll messages from given queues.
|
||||
* On cancel, do nothing.
|
||||
*/
|
||||
function listMessageService(
|
||||
$q, basePath, policy, zaqar, events, gettext, $qExtensions, modal, toast
|
||||
) {
|
||||
// schema
|
||||
var schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
postMessages: {
|
||||
title: gettext("List Messages"),
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// form
|
||||
var form = [
|
||||
{
|
||||
type: 'section',
|
||||
htmlClass: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'section',
|
||||
htmlClass: 'col-sm-12',
|
||||
items: [
|
||||
{
|
||||
type: 'template',
|
||||
templateUrl: basePath + 'actions/list-message.html'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// model
|
||||
var model;
|
||||
|
||||
var message = {
|
||||
success: gettext('Messages has been posted to queue %s successfully.')
|
||||
};
|
||||
|
||||
var service = {
|
||||
initAction: initAction,
|
||||
perform: perform,
|
||||
allowed: allowed
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
function initAction() {
|
||||
}
|
||||
|
||||
function allowed() {
|
||||
return $qExtensions.booleanAsPromise(true);
|
||||
}
|
||||
|
||||
function perform(selected) {
|
||||
model = {
|
||||
id: selected.id,
|
||||
name: selected.name
|
||||
};
|
||||
// modal config
|
||||
var config = {
|
||||
"title": gettext('List Messages'),
|
||||
"submitText": gettext('List Messages'),
|
||||
"schema": schema,
|
||||
"form": form,
|
||||
"model": model
|
||||
};
|
||||
return modal.open(config).then(submit);
|
||||
}
|
||||
|
||||
function submit(context) {
|
||||
var id = context.model.id;
|
||||
var name = context.model.name;
|
||||
delete context.model.id;
|
||||
delete context.model.name;
|
||||
return zaqar.postMessages(id, context.model).then(function() {
|
||||
toast.add('success', interpolate(message.success, [name]));
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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('horizon.dashboard.project.queues.actions.messageController', function() {
|
||||
var zaqar, controller, $scope, $q, deferred;
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
||||
beforeEach(module('horizon.dashboard.project.queues'));
|
||||
|
||||
beforeEach(inject(function ($injector, _$rootScope_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
$scope.model = {
|
||||
id: ''
|
||||
};
|
||||
zaqar = $injector.get('horizon.app.core.openstack-service-api.zaqar');
|
||||
controller = $injector.get('$controller');
|
||||
controller(
|
||||
'horizon.dashboard.project.queues.actions.messageController',
|
||||
{
|
||||
$scope: $scope,
|
||||
zaqar: zaqar
|
||||
});
|
||||
deferred = $q.defer();
|
||||
deferred.resolve({data: {id: '1'}});
|
||||
spyOn(zaqar, 'getMessages').and.returnValue(deferred.promise);
|
||||
asdf();
|
||||
}));
|
||||
|
||||
it('should load messages for queue', function() {
|
||||
expect(zaqar.getMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue_id is provided by scope variable', function() {
|
||||
$scope.model.id = '1';
|
||||
$scope.$apply();
|
||||
expect($scope.model.id).toBe('1');
|
||||
});
|
||||
});
|
||||
})();
|
@ -0,0 +1,28 @@
|
||||
<div class="hz-section col-sm-6" translate>
|
||||
<p>
|
||||
You can submit up to 10 messages in a single request, but you must always
|
||||
encapsulate the messages in a collection container (an array in JSON, even
|
||||
for a single message - without the JSON array, you receive the “Invalid
|
||||
request body” message). The resulting value of the Location header or
|
||||
response body might be used to retrieve the created messages for further
|
||||
processing.
|
||||
</p>
|
||||
<p>
|
||||
The client specifies only the body and TTL for the message. The server
|
||||
inserts metadata, such as ID and age.
|
||||
</p>
|
||||
<p>
|
||||
See a sample as below:
|
||||
</p>
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"body": {
|
||||
"event": "BackupProgress",
|
||||
"current_bytes": "2341134",
|
||||
"total_bytes": "99614720"
|
||||
}
|
||||
}
|
||||
]
|
||||
</pre>
|
||||
</div>
|
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
angular
|
||||
.module('horizon.dashboard.project.queues')
|
||||
.factory(
|
||||
'horizon.dashboard.project.queues.actions.postMessageService', postMessageService);
|
||||
|
||||
postMessageService.$inject = [
|
||||
'$q',
|
||||
'horizon.dashboard.project.queues.basePath',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.app.core.openstack-service-api.zaqar',
|
||||
'horizon.dashboard.project.queues.events',
|
||||
'horizon.framework.util.i18n.gettext',
|
||||
'horizon.framework.util.q.extensions',
|
||||
'horizon.framework.widgets.form.ModalFormService',
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name horizon.dashboard.project.queues.actions.postMessageService
|
||||
* @param {Object} $q
|
||||
* @param {String} basePath
|
||||
* @param {Object} policy
|
||||
* @param {Object} zaqar
|
||||
* @param {Object} events
|
||||
* @param {Object} gettext
|
||||
* @param {Object} $qExtensions
|
||||
* @param {Object} modal
|
||||
* @param {Object} toast
|
||||
* @returns {Object} post messages service
|
||||
* @description Brings up the post messages modal dialog.
|
||||
* On submit, post messages to given queues.
|
||||
* On cancel, do nothing.
|
||||
*/
|
||||
function postMessageService(
|
||||
$q, basePath, policy, zaqar, events, gettext, $qExtensions, modal, toast
|
||||
) {
|
||||
// schema
|
||||
var schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
postMessages: {
|
||||
title: gettext("Post Messages"),
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// form
|
||||
var form = [
|
||||
{
|
||||
type: 'section',
|
||||
htmlClass: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'section',
|
||||
htmlClass: 'col-sm-6',
|
||||
items: [
|
||||
{
|
||||
key: 'messages',
|
||||
type: 'textarea'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'template',
|
||||
templateUrl: basePath + 'actions/post-message.help.html'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// model
|
||||
var model = {};
|
||||
|
||||
var message = {
|
||||
success: gettext('Messages has been posted to queue %s successfully.')
|
||||
};
|
||||
|
||||
var service = {
|
||||
initAction: initAction,
|
||||
perform: perform,
|
||||
allowed: allowed
|
||||
};
|
||||
|
||||
var scope;
|
||||
return service;
|
||||
|
||||
//////////////
|
||||
|
||||
function initAction() {
|
||||
}
|
||||
|
||||
function allowed() {
|
||||
return $qExtensions.booleanAsPromise(true);
|
||||
}
|
||||
|
||||
function perform(selected, $scope) {
|
||||
scope = $scope;
|
||||
model = {
|
||||
id: selected.id,
|
||||
name: selected.name
|
||||
};
|
||||
// modal config
|
||||
var config = {
|
||||
"title": gettext('List Messages'),
|
||||
"submitText": gettext('Post'),
|
||||
"schema": schema,
|
||||
"form": form,
|
||||
"model": model
|
||||
};
|
||||
return modal.open(config).then(submit);
|
||||
}
|
||||
|
||||
function submit(context) {
|
||||
var id = context.model.id;
|
||||
var name = context.model.name;
|
||||
delete context.model.id;
|
||||
delete context.model.name;
|
||||
return zaqar.postMessages(id, context.model).then(function() {
|
||||
toast.add('success', interpolate(message.success, [name]));
|
||||
scope.$emit(events.POST_MESSAGE_SUCCESS, name);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
@ -50,6 +50,7 @@
|
||||
DELETE_SUCCESS: 'horizon.dashboard.project.queues.DELETE_SUCCESS',
|
||||
UPDATE_SUCCESS: 'horizon.dashboard.project.queues.UPDATE_SUCCESS',
|
||||
PURGE_SUCCESS: 'horizon.dashboard.project.queues.PURGE_SUCCESS',
|
||||
POST_MESSAGE_SUCCESS: 'horizon.dashboard.project.queues.POST_MESSAGE_SUCCESS',
|
||||
SUBSCRIPTION_CREATE_SUCCESS: 'horizon.dashboard.project.queues.SUBSCRIPTION_CREATE_SUCCESS'
|
||||
};
|
||||
}
|
||||
|
@ -2,3 +2,7 @@
|
||||
.subtitle {
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
textarea#messages {
|
||||
height: 28em;
|
||||
}
|
@ -55,12 +55,14 @@
|
||||
var deleteWatcher = $scope.$on(events.DELETE_SUCCESS, onDeleteSuccess);
|
||||
var updateWatcher = $scope.$on(events.UPDATE_SUCCESS, onUpdateSuccess);
|
||||
var purgeWatcher = $scope.$on(events.PURGE_SUCCESS, onPurgeSuccess);
|
||||
var postMessageWatcher = $scope.$on(events.POST_MESSAGE_SUCCESS, onPostMessageSuccess);
|
||||
var subWatcher = $scope.$on(events.SUBSCRIPTION_CREATE_SUCCESS, broadcastEvents);
|
||||
$scope.$on('$destroy', function destroy() {
|
||||
createWatcher();
|
||||
deleteWatcher();
|
||||
updateWatcher();
|
||||
purgeWatcher();
|
||||
postMessageWatcher();
|
||||
subWatcher();
|
||||
});
|
||||
}
|
||||
@ -146,6 +148,10 @@
|
||||
refreshSubscriptions(queueName);
|
||||
}
|
||||
|
||||
function onPostMessageSuccess(e, queueName) {
|
||||
e.stopPropagation();
|
||||
refreshQueue(queueName);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -109,6 +109,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<ng-include src="table.subsTemplate"></ng-include>
|
||||
<ng-include src="table.msgsTemplate"></ng-include>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user