mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
updates to getting host counts
This commit is contained in:
committed by
jaredevantabor
parent
e31bfa2f1c
commit
11592047d6
@@ -4,90 +4,21 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
export default [function(){
|
export default ['jobResultsService', function(jobResultsService){
|
||||||
var val = {};
|
var val = {};
|
||||||
|
|
||||||
// the playbook_on_stats event returns the count data in a weird format.
|
|
||||||
// format to what we need!
|
|
||||||
var getCountsFromStatsEvent = function(event_data) {
|
|
||||||
var hosts = {},
|
|
||||||
hostsArr;
|
|
||||||
|
|
||||||
// iterate over the event_data and populate an object with hosts
|
|
||||||
// and their status data
|
|
||||||
Object.keys(event_data).forEach(key => {
|
|
||||||
// failed passes boolean not integer
|
|
||||||
if (key === "failed") {
|
|
||||||
// array of hosts from failed type
|
|
||||||
hostsArr = Object.keys(event_data[key]);
|
|
||||||
hostsArr.forEach(host => {
|
|
||||||
if (!hosts[host]) {
|
|
||||||
// host has not been added to hosts object
|
|
||||||
// add now
|
|
||||||
hosts[host] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts[host][key] = event_data[key][host];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// array of hosts from each type ("changed", "dark", etc.)
|
|
||||||
hostsArr = Object.keys(event_data[key]);
|
|
||||||
hostsArr.forEach(host => {
|
|
||||||
if (!hosts[host]) {
|
|
||||||
// host has not been added to hosts object
|
|
||||||
// add now
|
|
||||||
hosts[host] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hosts[host][key]) {
|
|
||||||
// host doesn't have key
|
|
||||||
hosts[host][key] = 0;
|
|
||||||
}
|
|
||||||
hosts[host][key] += event_data[key][host];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// use the hosts data populate above to get the count
|
|
||||||
var count = {
|
|
||||||
ok : _.filter(hosts, function(o){
|
|
||||||
return !o.failures && !o.changed && o.ok > 0;
|
|
||||||
}),
|
|
||||||
skipped : _.filter(hosts, function(o){
|
|
||||||
return o.skipped > 0;
|
|
||||||
}),
|
|
||||||
unreachable : _.filter(hosts, function(o){
|
|
||||||
return o.dark > 0;
|
|
||||||
}),
|
|
||||||
failures : _.filter(hosts, function(o){
|
|
||||||
return o.failed === true;
|
|
||||||
}),
|
|
||||||
changed : _.filter(hosts, function(o){
|
|
||||||
return o.changed > 0;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// turn the count into an actual count, rather than a list of host
|
|
||||||
// names
|
|
||||||
Object.keys(count).forEach(key => {
|
|
||||||
count[key] = count[key].length;
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the count of the last event
|
// Get the count of the last event
|
||||||
var getPreviousCount = function(id) {
|
var getPreviousCount = function(counter) {
|
||||||
// get the ids of all the queue
|
// get the ids of all the queue
|
||||||
var ids = Object.keys(val.queue).map(id => parseInt(id));
|
var counters = Object.keys(val.queue).map(counter => parseInt(counter));
|
||||||
|
|
||||||
// iterate backwards to find the last count
|
// iterate backwards to find the last count
|
||||||
while(ids.indexOf(id - 1) > -1) {
|
while(counters.indexOf(counter - 1) > -1) {
|
||||||
id = id - 1;
|
counter = counter - 1;
|
||||||
if (val.queue[id].count) {
|
if (val.queue[counter].count) {
|
||||||
// need to create a new copy of count when returning
|
// need to create a new copy of count when returning
|
||||||
// so that it is accurate for the particular event
|
// so that it is accurate for the particular event
|
||||||
return _.clone(val.queue[id].count);
|
return _.clone(val.queue[counter].count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,33 +35,39 @@ export default [function(){
|
|||||||
// munge the raw event from the backend into the event_queue's format
|
// munge the raw event from the backend into the event_queue's format
|
||||||
var mungeEvent = function(event) {
|
var mungeEvent = function(event) {
|
||||||
var mungedEvent = {
|
var mungedEvent = {
|
||||||
|
counter: event.counter,
|
||||||
id: event.id,
|
id: event.id,
|
||||||
processed: false,
|
processed: false,
|
||||||
name: event.event_name,
|
name: event.event_name
|
||||||
count: getPreviousCount(event.id)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.event_name === 'playbook_on_start') {
|
if (event.event_name === 'playbook_on_start') {
|
||||||
// sets count initially so this is a change
|
// sets count initially so this is a change
|
||||||
|
mungedEvent.count = getPreviousCount(mungedEvent.counter);
|
||||||
mungedEvent.changes = ['count'];
|
mungedEvent.changes = ['count'];
|
||||||
} else if (event.event_name === 'runner_on_ok' ||
|
} else if (event.event_name === 'runner_on_ok' ||
|
||||||
event.event_name === 'runner_on_async_ok') {
|
event.event_name === 'runner_on_async_ok') {
|
||||||
|
mungedEvent.count = getPreviousCount(mungedEvent.counter);
|
||||||
mungedEvent.count.ok++;
|
mungedEvent.count.ok++;
|
||||||
mungedEvent.changes = ['count'];
|
mungedEvent.changes = ['count'];
|
||||||
} else if (event.event_name === 'runner_on_skipped') {
|
} else if (event.event_name === 'runner_on_skipped') {
|
||||||
|
mungedEvent.count = getPreviousCount(mungedEvent.counter);
|
||||||
mungedEvent.count.skipped++;
|
mungedEvent.count.skipped++;
|
||||||
mungedEvent.changes = ['count'];
|
mungedEvent.changes = ['count'];
|
||||||
} else if (event.event_name === 'runner_on_unreachable') {
|
} else if (event.event_name === 'runner_on_unreachable') {
|
||||||
|
mungedEvent.count = getPreviousCount(mungedEvent.counter);
|
||||||
mungedEvent.count.unreachable++;
|
mungedEvent.count.unreachable++;
|
||||||
mungedEvent.changes = ['count'];
|
mungedEvent.changes = ['count'];
|
||||||
} else if (event.event_name === 'runner_on_error' ||
|
} else if (event.event_name === 'runner_on_error' ||
|
||||||
event.event_name === 'runner_on_async_failed') {
|
event.event_name === 'runner_on_async_failed') {
|
||||||
|
mungedEvent.count = getPreviousCount(mungedEvent.counter);
|
||||||
mungedEvent.count.failed++;
|
mungedEvent.count.failed++;
|
||||||
mungedEvent.changes = ['count'];
|
mungedEvent.changes = ['count'];
|
||||||
} else if (event.event_name === 'playbook_on_stats') {
|
} else if (event.event_name === 'playbook_on_stats') {
|
||||||
// get the data for populating the host status bar
|
// get the data for populating the host status bar
|
||||||
mungedEvent.count = getCountsFromStatsEvent(event.event_data);
|
mungedEvent.count = jobResultsService
|
||||||
mungedEvent.changes = ['count'];
|
.getCountsFromStatsEvent(event.event_data);
|
||||||
|
mungedEvent.changes = ['count', 'countFinished'];
|
||||||
}
|
}
|
||||||
return mungedEvent;
|
return mungedEvent;
|
||||||
};
|
};
|
||||||
@@ -143,15 +80,19 @@ export default [function(){
|
|||||||
},
|
},
|
||||||
// populates the event queue
|
// populates the event queue
|
||||||
populate: function(event) {
|
populate: function(event) {
|
||||||
var mungedEvent = mungeEvent(event);
|
// don't populate the event if it's already been added either
|
||||||
val.queue[event.id] = mungedEvent;
|
// by rest or by websocket event
|
||||||
|
if (!val.queue[event.counter]) {
|
||||||
|
var mungedEvent = mungeEvent(event);
|
||||||
|
val.queue[mungedEvent.counter] = mungedEvent;
|
||||||
|
|
||||||
return mungedEvent;
|
return mungedEvent;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// the event has been processed in the view and should be marked as
|
// the event has been processed in the view and should be marked as
|
||||||
// completed in the queue
|
// completed in the queue
|
||||||
markProcessed: function(event) {
|
markProcessed: function(event) {
|
||||||
val.queue[event.id].processed = true;
|
val.queue[event.counter].processed = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,41 +7,46 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.HostStatusBar-ok,
|
||||||
|
.HostStatusBar-changed,
|
||||||
|
.HostStatusBar-unreachable,
|
||||||
|
.HostStatusBar-failures,
|
||||||
|
.HostStatusBar-skipped,
|
||||||
|
.HostStatusBar-noData {
|
||||||
|
height: 15px;
|
||||||
|
border-top: 5px solid @default-bg;
|
||||||
|
border-bottom: 5px solid @default-bg;
|
||||||
|
}
|
||||||
|
|
||||||
.HostStatusBar-ok {
|
.HostStatusBar-ok {
|
||||||
background-color: @default-succ;
|
background-color: @default-succ;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-changed {
|
.HostStatusBar-changed {
|
||||||
background-color: @default-warning;
|
background-color: @default-warning;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-unreachable {
|
.HostStatusBar-unreachable {
|
||||||
background-color: @default-unreachable;
|
background-color: @default-unreachable;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-failures {
|
.HostStatusBar-failures {
|
||||||
background-color: @default-err;
|
background-color: @default-err;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-skipped {
|
.HostStatusBar-skipped {
|
||||||
background-color: @default-link;
|
background-color: @default-link;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-noData {
|
.HostStatusBar-noData {
|
||||||
background-color: @default-icon-hov;
|
background-color: @default-icon-hov;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
height: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HostStatusBar-tooltipLabel {
|
.HostStatusBar-tooltipLabel {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) {
|
export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) {
|
||||||
var getTowerLinks = function() {
|
var getTowerLinks = function() {
|
||||||
var getTowerLink = function(key) {
|
var getTowerLink = function(key) {
|
||||||
if ($scope.job.related[key]) {
|
if ($scope.job.related[key]) {
|
||||||
@@ -34,6 +34,11 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
|
|||||||
$scope.verbosity_label = getTowerLabel('verbosity');
|
$scope.verbosity_label = getTowerLabel('verbosity');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getTotalHostCount = function(count) {
|
||||||
|
return Object
|
||||||
|
.keys(count).reduce((acc, i) => acc += count[i], 0);
|
||||||
|
};
|
||||||
|
|
||||||
// put initially resolved request data on scope
|
// put initially resolved request data on scope
|
||||||
$scope.job = jobData;
|
$scope.job = jobData;
|
||||||
$scope.jobOptions = jobDataOptions.actions.GET;
|
$scope.jobOptions = jobDataOptions.actions.GET;
|
||||||
@@ -66,6 +71,11 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
|
|||||||
jobResultsService.cancelJob($scope.job);
|
jobResultsService.cancelJob($scope.job);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get initial count from resolve
|
||||||
|
$scope.count = count.val;
|
||||||
|
$scope.hostCount = getTotalHostCount(count.val);
|
||||||
|
$scope.countFinished = count.countFinished;
|
||||||
|
|
||||||
// EVENT STUFF BELOW
|
// EVENT STUFF BELOW
|
||||||
|
|
||||||
// just putting the event queue on scope so it can be inspected in the
|
// just putting the event queue on scope so it can be inspected in the
|
||||||
@@ -79,8 +89,14 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
|
|||||||
// make changes to ui based on the event returned from the queue
|
// make changes to ui based on the event returned from the queue
|
||||||
if (mungedEvent.changes) {
|
if (mungedEvent.changes) {
|
||||||
mungedEvent.changes.forEach(change => {
|
mungedEvent.changes.forEach(change => {
|
||||||
if (change === 'count') {
|
if (change === 'count' && !$scope.countFinished) {
|
||||||
$scope.count = mungedEvent.count;
|
$scope.count = mungedEvent.count;
|
||||||
|
$scope.hostCount = getTotalHostCount(mungedEvent
|
||||||
|
.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change === 'countFnished') {
|
||||||
|
$scope.countFinished = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,15 +106,21 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh
|
|||||||
};
|
};
|
||||||
|
|
||||||
// grab completed event data and process each event
|
// grab completed event data and process each event
|
||||||
jobResultsService.getEvents($scope.job)
|
var getEvents = function(url) {
|
||||||
.then(events => {
|
jobResultsService.getEvents(url)
|
||||||
events.forEach(event => {
|
.then(events => {
|
||||||
// get the name in the same format as the data
|
events.results.forEach(event => {
|
||||||
// coming over the websocket
|
// get the name in the same format as the data
|
||||||
event.event_name = event.event;
|
// coming over the websocket
|
||||||
processEvent(event);
|
event.event_name = event.event;
|
||||||
|
processEvent(event);
|
||||||
|
});
|
||||||
|
if (events.next) {
|
||||||
|
getEvents(events.next);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
getEvents($scope.job.related.job_events);
|
||||||
|
|
||||||
// process incoming job events
|
// process incoming job events
|
||||||
$rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) {
|
$rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) {
|
||||||
|
|||||||
@@ -459,7 +459,7 @@
|
|||||||
Plays
|
Plays
|
||||||
</div>
|
</div>
|
||||||
<span class="badge List-titleBadge">
|
<span class="badge List-titleBadge">
|
||||||
{{jobData.playCount || 0}}
|
{{ playCount || 0}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- TASKS COUNT -->
|
<!-- TASKS COUNT -->
|
||||||
@@ -467,7 +467,7 @@
|
|||||||
Tasks
|
Tasks
|
||||||
</div>
|
</div>
|
||||||
<span class="badge List-titleBadge">
|
<span class="badge List-titleBadge">
|
||||||
{{jobData.taskCount || 0}}
|
{{ taskCount || 0}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- HOSTS COUNT -->
|
<!-- HOSTS COUNT -->
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
Hosts
|
Hosts
|
||||||
</div>
|
</div>
|
||||||
<span class="badge List-titleBadge">
|
<span class="badge List-titleBadge">
|
||||||
{{jobData.hostCount || 0}}
|
{{ hostCount || 0}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- ELAPSED TIME -->
|
<!-- ELAPSED TIME -->
|
||||||
|
|||||||
@@ -35,6 +35,43 @@ export default {
|
|||||||
});
|
});
|
||||||
return val.promise;
|
return val.promise;
|
||||||
}],
|
}],
|
||||||
|
count: ['jobData', 'jobResultsService', 'Rest', '$q', function(jobData, jobResultsService, Rest, $q) {
|
||||||
|
var defer = $q.defer();
|
||||||
|
if (jobData.finished) {
|
||||||
|
// if the job is finished, grab the playbook_on_stats
|
||||||
|
// role to get the final count
|
||||||
|
Rest.setUrl(jobData.related.job_events +
|
||||||
|
"?event=playbook_on_stats");
|
||||||
|
Rest.get()
|
||||||
|
.success(function(data) {
|
||||||
|
defer.resolve({
|
||||||
|
val: jobResultsService
|
||||||
|
.getCountsFromStatsEvent(data
|
||||||
|
.results[0].event_data),
|
||||||
|
countFinished: true});
|
||||||
|
})
|
||||||
|
.error(function() {
|
||||||
|
defer.resolve({val: {
|
||||||
|
ok: 0,
|
||||||
|
skipped: 0,
|
||||||
|
unreachable: 0,
|
||||||
|
failures: 0,
|
||||||
|
changed: 0
|
||||||
|
}, countFinished: false});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// job isn't finished so just send an empty count and read
|
||||||
|
// from events
|
||||||
|
defer.resolve({val: {
|
||||||
|
ok: 0,
|
||||||
|
skipped: 0,
|
||||||
|
unreachable: 0,
|
||||||
|
failures: 0,
|
||||||
|
changed: 0
|
||||||
|
}, countFinished: false});
|
||||||
|
}
|
||||||
|
return defer.promise;
|
||||||
|
}],
|
||||||
jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
|
jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) {
|
||||||
var getNext = function(data, arr, resolve) {
|
var getNext = function(data, arr, resolve) {
|
||||||
Rest.setUrl(data.next);
|
Rest.setUrl(data.next);
|
||||||
|
|||||||
@@ -7,13 +7,81 @@
|
|||||||
|
|
||||||
export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) {
|
export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) {
|
||||||
var val = {
|
var val = {
|
||||||
getEvents: function(job) {
|
// the playbook_on_stats event returns the count data in a weird format.
|
||||||
|
// format to what we need!
|
||||||
|
getCountsFromStatsEvent: function(event_data) {
|
||||||
|
var hosts = {},
|
||||||
|
hostsArr;
|
||||||
|
|
||||||
|
// iterate over the event_data and populate an object with hosts
|
||||||
|
// and their status data
|
||||||
|
Object.keys(event_data).forEach(key => {
|
||||||
|
// failed passes boolean not integer
|
||||||
|
if (key === "failed") {
|
||||||
|
// array of hosts from failed type
|
||||||
|
hostsArr = Object.keys(event_data[key]);
|
||||||
|
hostsArr.forEach(host => {
|
||||||
|
if (!hosts[host]) {
|
||||||
|
// host has not been added to hosts object
|
||||||
|
// add now
|
||||||
|
hosts[host] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts[host][key] = event_data[key][host];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// array of hosts from each type ("changed", "dark", etc.)
|
||||||
|
hostsArr = Object.keys(event_data[key]);
|
||||||
|
hostsArr.forEach(host => {
|
||||||
|
if (!hosts[host]) {
|
||||||
|
// host has not been added to hosts object
|
||||||
|
// add now
|
||||||
|
hosts[host] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hosts[host][key]) {
|
||||||
|
// host doesn't have key
|
||||||
|
hosts[host][key] = 0;
|
||||||
|
}
|
||||||
|
hosts[host][key] += event_data[key][host];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// use the hosts data populate above to get the count
|
||||||
|
var count = {
|
||||||
|
ok : _.filter(hosts, function(o){
|
||||||
|
return !o.failures && !o.changed && o.ok > 0;
|
||||||
|
}),
|
||||||
|
skipped : _.filter(hosts, function(o){
|
||||||
|
return o.skipped > 0;
|
||||||
|
}),
|
||||||
|
unreachable : _.filter(hosts, function(o){
|
||||||
|
return o.dark > 0;
|
||||||
|
}),
|
||||||
|
failures : _.filter(hosts, function(o){
|
||||||
|
return o.failed === true;
|
||||||
|
}),
|
||||||
|
changed : _.filter(hosts, function(o){
|
||||||
|
return o.changed > 0;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// turn the count into an actual count, rather than a list of host
|
||||||
|
// names
|
||||||
|
Object.keys(count).forEach(key => {
|
||||||
|
count[key] = count[key].length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
getEvents: function(url) {
|
||||||
var val = $q.defer();
|
var val = $q.defer();
|
||||||
|
|
||||||
Rest.setUrl(job.related.job_events);
|
Rest.setUrl(url);
|
||||||
Rest.get()
|
Rest.get()
|
||||||
.success(function(data) {
|
.success(function(data) {
|
||||||
val.resolve(data.results);
|
val.resolve({results: data.results, next: data.next});
|
||||||
})
|
})
|
||||||
.error(function(obj, status) {
|
.error(function(obj, status) {
|
||||||
ProcessErrors(null, obj, status, null, {
|
ProcessErrors(null, obj, status, null, {
|
||||||
|
|||||||
Reference in New Issue
Block a user