mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
cleanup activity stream
This commit is contained in:
@@ -16,9 +16,7 @@
|
|||||||
* @description This form is for activity detail modal that can be shown on most pages.
|
* @description This form is for activity detail modal that can be shown on most pages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default
|
export default ['i18n', function(i18n) {
|
||||||
angular.module('ActivityDetailDefinition', [])
|
|
||||||
.factory('ActivityDetailForm', ['i18n', function(i18n) {
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
name: 'activity',
|
name: 'activity',
|
||||||
@@ -48,4 +46,4 @@ export default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};}]); //Form
|
};}];
|
||||||
@@ -9,45 +9,47 @@
|
|||||||
* @name controllers.function:Activity Stream
|
* @name controllers.function:Activity Stream
|
||||||
* @description This controller controls the activity stream.
|
* @description This controller controls the activity stream.
|
||||||
*/
|
*/
|
||||||
function activityStreamController($scope, $state, subTitle, Stream, GetTargetTitle, list, Dataset) {
|
export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle',
|
||||||
|
'StreamList', 'Dataset',
|
||||||
|
function activityStreamController($scope, $state, subTitle, Stream,
|
||||||
|
GetTargetTitle, list, Dataset) {
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initOmitSmartTags();
|
initOmitSmartTags();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
// search init
|
// search init
|
||||||
$scope.list = list;
|
$scope.list = list;
|
||||||
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
$scope[`${list.iterator}_dataset`] = Dataset.data;
|
||||||
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
|
||||||
|
|
||||||
// subTitle is passed in via a resolve on the route. If there is no subtitle
|
// subTitle is passed in via a resolve on the route. If there is no subtitle
|
||||||
// generated in the resolve then we go get the targets generic title.
|
// generated in the resolve then we go get the targets generic title.
|
||||||
|
|
||||||
// Get the streams sub-title based on the target. This scope variable is leveraged
|
// Get the streams sub-title based on the target. This scope variable is leveraged
|
||||||
// when we define the activity stream list. Specifically it is included in the list
|
// when we define the activity stream list. Specifically it is included in the list
|
||||||
// title.
|
// title.
|
||||||
$scope.streamSubTitle = subTitle ? subTitle : GetTargetTitle($state.params.target);
|
$scope.streamSubTitle = subTitle ? subTitle : GetTargetTitle($state.params.target);
|
||||||
|
|
||||||
// Open the stream
|
// Open the stream
|
||||||
Stream({
|
Stream({
|
||||||
scope: $scope
|
scope: $scope
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specification of smart-tags omission from the UI is done in the route/state init.
|
// Specification of smart-tags omission from the UI is done in the route/state init.
|
||||||
// A limitation is that this specficiation is static and the key for which to be omitted from
|
// A limitation is that this specficiation is static and the key for which to be omitted from
|
||||||
// the smart-tags must be known at that time.
|
// the smart-tags must be known at that time.
|
||||||
// In the case of activity stream, we won't to dynamically ommit the resource for which we are
|
// In the case of activity stream, we won't to dynamically ommit the resource for which we are
|
||||||
// displaying the activity stream for. i.e. 'project', 'credential', etc.
|
// displaying the activity stream for. i.e. 'project', 'credential', etc.
|
||||||
function initOmitSmartTags() {
|
function initOmitSmartTags() {
|
||||||
let defaults, route = _.find($state.$current.path, (step) => {
|
let defaults, route = _.find($state.$current.path, (step) => {
|
||||||
return step.params.hasOwnProperty('activity_search');
|
return step.params.hasOwnProperty('activity_search');
|
||||||
});
|
});
|
||||||
if (route && $state.params.target !== undefined) {
|
if (route && $state.params.target !== undefined) {
|
||||||
defaults = route.params.activity_search.config.value;
|
defaults = route.params.activity_search.config.value;
|
||||||
defaults[$state.params.target] = null;
|
defaults[$state.params.target] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle', 'StreamList', 'Dataset', activityStreamController];
|
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export default {
|
|||||||
return qs.search(path, stateParams);
|
return qs.search(path, stateParams);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
features: ['FeaturesService', 'ProcessErrors', '$state', '$rootScope',
|
features: ['FeaturesService', '$state', '$rootScope',
|
||||||
function(FeaturesService, ProcessErrors, $state, $rootScope) {
|
function(FeaturesService, $state, $rootScope) {
|
||||||
var features = FeaturesService.get();
|
var features = FeaturesService.get();
|
||||||
if (features) {
|
if (features) {
|
||||||
if (FeaturesService.featureEnabled('activity_streams')) {
|
if (FeaturesService.featureEnabled('activity_streams')) {
|
||||||
@@ -81,12 +81,10 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
subTitle: ['$stateParams',
|
subTitle: ['$stateParams', 'Rest', 'ModelToBasePathKey', 'GetBasePath',
|
||||||
'Rest',
|
|
||||||
'ModelToBasePathKey',
|
|
||||||
'GetBasePath',
|
|
||||||
'ProcessErrors',
|
'ProcessErrors',
|
||||||
function($stateParams, rest, ModelToBasePathKey, getBasePath, ProcessErrors) {
|
function($stateParams, rest, ModelToBasePathKey, getBasePath,
|
||||||
|
ProcessErrors) {
|
||||||
// If we have a target and an ID then we want to go grab the name of the object
|
// If we have a target and an ID then we want to go grab the name of the object
|
||||||
// that we're examining with the activity stream. This name will be used in the
|
// that we're examining with the activity stream. This name will be used in the
|
||||||
// subtitle.
|
// subtitle.
|
||||||
|
|||||||
@@ -1,78 +1,77 @@
|
|||||||
export default
|
export default function BuildAnchor($log, $filter) {
|
||||||
function BuildAnchor($log, $filter) {
|
// Returns a full <a href=''>resource_name</a> HTML string if link can be derived from supplied context
|
||||||
// Returns a full <a href=''>resource_name</a> HTML string if link can be derived from supplied context
|
// returns name of resource if activity stream object doesn't contain enough data to build a UI url
|
||||||
// returns name of resource if activity stream object doesn't contain enough data to build a UI url
|
// arguments are: a summary_field object, a resource type, an activity stream object
|
||||||
// arguments are: a summary_field object, a resource type, an activity stream object
|
return function (obj, resource, activity) {
|
||||||
return function (obj, resource, activity) {
|
var url = '/#/';
|
||||||
var url = '/#/';
|
// try/except pattern asserts that:
|
||||||
// try/except pattern asserts that:
|
// if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource
|
||||||
// if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource
|
try {
|
||||||
try {
|
// catch-all case to avoid generating urls if a resource has been deleted
|
||||||
// catch-all case to avoid generating urls if a resource has been deleted
|
// if a resource still exists, it'll be serialized in the activity's summary_fields
|
||||||
// if a resource still exists, it'll be serialized in the activity's summary_fields
|
if (!activity.summary_fields[resource]){
|
||||||
if (!activity.summary_fields[resource]){
|
throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'};
|
||||||
throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'};
|
|
||||||
}
|
|
||||||
switch (resource) {
|
|
||||||
case 'custom_inventory_script':
|
|
||||||
url += 'inventory_scripts/' + obj.id + '/';
|
|
||||||
break;
|
|
||||||
case 'group':
|
|
||||||
if (activity.operation === 'create' || activity.operation === 'delete'){
|
|
||||||
// the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey'
|
|
||||||
var inventory_id = _.last(activity.changes.inventory.split('-'));
|
|
||||||
url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'host':
|
|
||||||
url += 'home/hosts/' + obj.id;
|
|
||||||
break;
|
|
||||||
case 'job':
|
|
||||||
url += 'jobs/' + obj.id;
|
|
||||||
break;
|
|
||||||
case 'inventory':
|
|
||||||
url += 'inventories/' + obj.id + '/';
|
|
||||||
break;
|
|
||||||
case 'schedule':
|
|
||||||
// schedule urls depend on the resource they're associated with
|
|
||||||
if (activity.summary_fields.job_template){
|
|
||||||
url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id;
|
|
||||||
}
|
|
||||||
else if (activity.summary_fields.project){
|
|
||||||
url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id;
|
|
||||||
}
|
|
||||||
else if (activity.summary_fields.system_job_template){
|
|
||||||
url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id;
|
|
||||||
}
|
|
||||||
// urls for inventory sync schedules currently depend on having an inventory id and group id
|
|
||||||
else {
|
|
||||||
throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'notification_template':
|
|
||||||
url += `notification_templates/${obj.id}`;
|
|
||||||
break;
|
|
||||||
case 'role':
|
|
||||||
throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'};
|
|
||||||
case 'job_template':
|
|
||||||
url += `templates/job_template/${obj.id}`;
|
|
||||||
break;
|
|
||||||
case 'workflow_job_template':
|
|
||||||
url += `templates/workflow_job_template/${obj.id}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
url += resource + 's/' + obj.id + '/';
|
|
||||||
}
|
|
||||||
return ' <a href=\"' + url + '\"> ' + $filter('sanitize')(obj.name || obj.username) + ' </a> ';
|
|
||||||
}
|
}
|
||||||
catch(err){
|
switch (resource) {
|
||||||
$log.debug(err);
|
case 'custom_inventory_script':
|
||||||
return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' ';
|
url += 'inventory_scripts/' + obj.id + '/';
|
||||||
|
break;
|
||||||
|
case 'group':
|
||||||
|
if (activity.operation === 'create' || activity.operation === 'delete'){
|
||||||
|
// the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey'
|
||||||
|
var inventory_id = _.last(activity.changes.inventory.split('-'));
|
||||||
|
url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
url += 'home/hosts/' + obj.id;
|
||||||
|
break;
|
||||||
|
case 'job':
|
||||||
|
url += 'jobs/' + obj.id;
|
||||||
|
break;
|
||||||
|
case 'inventory':
|
||||||
|
url += 'inventories/' + obj.id + '/';
|
||||||
|
break;
|
||||||
|
case 'schedule':
|
||||||
|
// schedule urls depend on the resource they're associated with
|
||||||
|
if (activity.summary_fields.job_template){
|
||||||
|
url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id;
|
||||||
|
}
|
||||||
|
else if (activity.summary_fields.project){
|
||||||
|
url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id;
|
||||||
|
}
|
||||||
|
else if (activity.summary_fields.system_job_template){
|
||||||
|
url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id;
|
||||||
|
}
|
||||||
|
// urls for inventory sync schedules currently depend on having an inventory id and group id
|
||||||
|
else {
|
||||||
|
throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'notification_template':
|
||||||
|
url += `notification_templates/${obj.id}`;
|
||||||
|
break;
|
||||||
|
case 'role':
|
||||||
|
throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'};
|
||||||
|
case 'job_template':
|
||||||
|
url += `templates/job_template/${obj.id}`;
|
||||||
|
break;
|
||||||
|
case 'workflow_job_template':
|
||||||
|
url += `templates/workflow_job_template/${obj.id}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
url += resource + 's/' + obj.id + '/';
|
||||||
}
|
}
|
||||||
};
|
return ' <a href=\"' + url + '\"> ' + $filter('sanitize')(obj.name || obj.username) + ' </a> ';
|
||||||
}
|
}
|
||||||
|
catch(err){
|
||||||
|
$log.debug(err);
|
||||||
|
return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' ';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
BuildAnchor.$inject = ['$log', '$filter'];
|
BuildAnchor.$inject = ['$log', '$filter'];
|
||||||
|
|||||||
@@ -1,126 +1,125 @@
|
|||||||
export default
|
export default function BuildDescription(BuildAnchor, $log, i18n) {
|
||||||
function BuildDescription(BuildAnchor, $log, i18n) {
|
return function (activity) {
|
||||||
return function (activity) {
|
|
||||||
|
|
||||||
var pastTense = function(operation){
|
var pastTense = function(operation){
|
||||||
return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed ';
|
return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed ';
|
||||||
};
|
};
|
||||||
// convenience method to see if dis+association operation involves 2 groups
|
// convenience method to see if dis+association operation involves 2 groups
|
||||||
// the group cases are slightly different because groups can be dis+associated into each other
|
// the group cases are slightly different because groups can be dis+associated into each other
|
||||||
var isGroupRelationship = function(activity){
|
var isGroupRelationship = function(activity){
|
||||||
return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1;
|
return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1;
|
||||||
};
|
|
||||||
|
|
||||||
// Activity stream objects will outlive the resources they reference
|
|
||||||
// in that case, summary_fields will not be available - show generic error text instead
|
|
||||||
try {
|
|
||||||
activity.description = pastTense(activity.operation);
|
|
||||||
switch(activity.object_association){
|
|
||||||
// explicit role dis+associations
|
|
||||||
case 'role':
|
|
||||||
// object1 field is resource targeted by the dis+association
|
|
||||||
// object2 field is the resource the role is inherited from
|
|
||||||
// summary_field.role[0] contains ref info about the role
|
|
||||||
switch(activity.operation){
|
|
||||||
// expected outcome: "disassociated <object2> role_name from <object1>"
|
|
||||||
case 'disassociate':
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// expected outcome: "associated <object2> role_name to <object1>"
|
|
||||||
case 'associate':
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// inherited role dis+associations (logic identical to case 'role')
|
|
||||||
case 'parents':
|
|
||||||
// object1 field is resource targeted by the dis+association
|
|
||||||
// object2 field is the resource the role is inherited from
|
|
||||||
// summary_field.role[0] contains ref info about the role
|
|
||||||
switch(activity.operation){
|
|
||||||
// expected outcome: "disassociated <object2> role_name from <object1>"
|
|
||||||
case 'disassociate':
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) +
|
|
||||||
'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// expected outcome: "associated <object2> role_name to <object1>"
|
|
||||||
case 'associate':
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) +
|
|
||||||
'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
|
||||||
' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// CRUD operations / resource on resource dis+associations
|
|
||||||
default:
|
|
||||||
switch(activity.operation){
|
|
||||||
// expected outcome: "disassociated <object2> from <object1>"
|
|
||||||
case 'disassociate' :
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) +
|
|
||||||
'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) +
|
|
||||||
'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// expected outcome "associated <object2> to <object1>"
|
|
||||||
case 'associate':
|
|
||||||
// groups are the only resource that can be associated/disassociated into each other
|
|
||||||
if (isGroupRelationship(activity)){
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) +
|
|
||||||
'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) +
|
|
||||||
'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'delete':
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity);
|
|
||||||
break;
|
|
||||||
// expected outcome: "operation <object1>"
|
|
||||||
case 'update':
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
|
||||||
break;
|
|
||||||
case 'create':
|
|
||||||
activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
$log.debug(err);
|
|
||||||
activity.description = i18n._('Event summary not available');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
BuildDescription.$inject = ['BuildAnchor', '$log', 'i18n'];
|
// Activity stream objects will outlive the resources they reference
|
||||||
|
// in that case, summary_fields will not be available - show generic error text instead
|
||||||
|
try {
|
||||||
|
activity.description = pastTense(activity.operation);
|
||||||
|
switch(activity.object_association){
|
||||||
|
// explicit role dis+associations
|
||||||
|
case 'role':
|
||||||
|
// object1 field is resource targeted by the dis+association
|
||||||
|
// object2 field is the resource the role is inherited from
|
||||||
|
// summary_field.role[0] contains ref info about the role
|
||||||
|
switch(activity.operation){
|
||||||
|
// expected outcome: "disassociated <object2> role_name from <object1>"
|
||||||
|
case 'disassociate':
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// expected outcome: "associated <object2> role_name to <object1>"
|
||||||
|
case 'associate':
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// inherited role dis+associations (logic identical to case 'role')
|
||||||
|
case 'parents':
|
||||||
|
// object1 field is resource targeted by the dis+association
|
||||||
|
// object2 field is the resource the role is inherited from
|
||||||
|
// summary_field.role[0] contains ref info about the role
|
||||||
|
switch(activity.operation){
|
||||||
|
// expected outcome: "disassociated <object2> role_name from <object1>"
|
||||||
|
case 'disassociate':
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) +
|
||||||
|
'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// expected outcome: "associated <object2> role_name to <object1>"
|
||||||
|
case 'associate':
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) +
|
||||||
|
'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field +
|
||||||
|
' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// CRUD operations / resource on resource dis+associations
|
||||||
|
default:
|
||||||
|
switch(activity.operation){
|
||||||
|
// expected outcome: "disassociated <object2> from <object1>"
|
||||||
|
case 'disassociate' :
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) +
|
||||||
|
'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) +
|
||||||
|
'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// expected outcome "associated <object2> to <object1>"
|
||||||
|
case 'associate':
|
||||||
|
// groups are the only resource that can be associated/disassociated into each other
|
||||||
|
if (isGroupRelationship(activity)){
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) +
|
||||||
|
'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) +
|
||||||
|
'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity);
|
||||||
|
break;
|
||||||
|
// expected outcome: "operation <object1>"
|
||||||
|
case 'update':
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity);
|
||||||
|
break;
|
||||||
|
case 'create':
|
||||||
|
activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(err){
|
||||||
|
$log.debug(err);
|
||||||
|
activity.description = i18n._('Event summary not available');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildDescription.$inject = ['BuildAnchor', '$log', 'i18n'];
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
export default
|
export default function ShowDetail($filter, $rootScope, Rest, Alert,
|
||||||
function ShowDetail($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) {
|
GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm,
|
||||||
return function (params, scope) {
|
Empty, Find) {
|
||||||
|
return function (params, scope) {
|
||||||
|
|
||||||
var activity_id = params.activity_id,
|
var activity_id = params.activity_id,
|
||||||
activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }),
|
activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }),
|
||||||
element;
|
element;
|
||||||
|
|
||||||
if (activity) {
|
if (activity) {
|
||||||
|
|
||||||
// Grab our element out of the dom
|
// Grab our element out of the dom
|
||||||
element = angular.element(document.getElementById('stream-detail-modal'));
|
element = angular.element(document.getElementById('stream-detail-modal'));
|
||||||
|
|
||||||
// Grab the modal's scope so that we can set a few variables
|
// Grab the modal's scope so that we can set a few variables
|
||||||
scope = element.scope();
|
scope = element.scope();
|
||||||
|
|
||||||
scope.changes = activity.changes;
|
scope.changes = activity.changes;
|
||||||
scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') +
|
scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') +
|
||||||
' on ' + $filter('longDate')(activity.timestamp);
|
' on ' + $filter('longDate')(activity.timestamp);
|
||||||
scope.operation = activity.description;
|
scope.operation = activity.description;
|
||||||
scope.header = "Event " + activity.id;
|
scope.header = "Event " + activity.id;
|
||||||
|
|
||||||
// Open the modal
|
// Open the modal
|
||||||
$('#stream-detail-modal').modal({
|
$('#stream-detail-modal').modal({
|
||||||
show: true,
|
show: true,
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
keyboard: true
|
keyboard: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!scope.$$phase) {
|
if (!scope.$$phase) {
|
||||||
scope.$digest();
|
scope.$digest();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowDetail.$inject = ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate',
|
ShowDetail.$inject = ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate',
|
||||||
'ActivityDetailForm', 'Empty', 'Find'];
|
'ActivityDetailForm', 'Empty', 'Find'];
|
||||||
|
|||||||
@@ -1,53 +1,52 @@
|
|||||||
export default
|
export default function Stream($rootScope, $location, $state, Rest, GetBasePath,
|
||||||
function Stream($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors,
|
ProcessErrors, Wait, StreamList, GenerateList, FormatDate, BuildDescription,
|
||||||
Wait, StreamList, GenerateList, FormatDate,
|
ShowDetail) {
|
||||||
BuildDescription, ShowDetail) {
|
return function (params) {
|
||||||
return function (params) {
|
|
||||||
|
|
||||||
var scope = params.scope;
|
var scope = params.scope;
|
||||||
|
|
||||||
$rootScope.flashMessage = null;
|
$rootScope.flashMessage = null;
|
||||||
|
|
||||||
// descriptive title describing what AS is showing
|
// descriptive title describing what AS is showing
|
||||||
scope.streamTitle = (params && params.title) ? params.title : null;
|
scope.streamTitle = (params && params.title) ? params.title : null;
|
||||||
|
|
||||||
scope.refreshStream = function () {
|
|
||||||
$state.go('.', null, {reload: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
scope.showDetail = function (id) {
|
|
||||||
ShowDetail({
|
|
||||||
scope: scope,
|
|
||||||
activity_id: id
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if(scope.activities && scope.activities.length > 0) {
|
|
||||||
buildUserAndDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.$watch('activities', function(){
|
|
||||||
// Watch for future update to scope.activities (like page change, column sort, search, etc)
|
|
||||||
buildUserAndDescription();
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildUserAndDescription(){
|
|
||||||
scope.activities.forEach(function(activity, i) {
|
|
||||||
// build activity.user
|
|
||||||
if (scope.activities[i].summary_fields.actor) {
|
|
||||||
scope.activities[i].user = "<a href=\"/#/users/" + scope.activities[i].summary_fields.actor.id + "\">" +
|
|
||||||
scope.activities[i].summary_fields.actor.username + "</a>";
|
|
||||||
} else {
|
|
||||||
scope.activities[i].user = 'system';
|
|
||||||
}
|
|
||||||
// build description column / action text
|
|
||||||
BuildDescription(scope.activities[i]);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
scope.refreshStream = function () {
|
||||||
|
$state.go('.', null, {reload: true});
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
scope.showDetail = function (id) {
|
||||||
|
ShowDetail({
|
||||||
|
scope: scope,
|
||||||
|
activity_id: id
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if(scope.activities && scope.activities.length > 0) {
|
||||||
|
buildUserAndDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$watch('activities', function(){
|
||||||
|
// Watch for future update to scope.activities (like page change, column sort, search, etc)
|
||||||
|
buildUserAndDescription();
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildUserAndDescription(){
|
||||||
|
scope.activities.forEach(function(activity, i) {
|
||||||
|
// build activity.user
|
||||||
|
if (scope.activities[i].summary_fields.actor) {
|
||||||
|
scope.activities[i].user = "<a href=\"/#/users/" + scope.activities[i].summary_fields.actor.id + "\">" +
|
||||||
|
scope.activities[i].summary_fields.actor.username + "</a>";
|
||||||
|
} else {
|
||||||
|
scope.activities[i].user = 'system';
|
||||||
|
}
|
||||||
|
// build description column / action text
|
||||||
|
BuildDescription(scope.activities[i]);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Stream.$inject = ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath',
|
Stream.$inject = ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath',
|
||||||
'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription',
|
'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription',
|
||||||
|
|||||||
@@ -1,51 +1,50 @@
|
|||||||
export default
|
export default function GetTargetTitle(i18n) {
|
||||||
function GetTargetTitle(i18n) {
|
return function (target) {
|
||||||
return function (target) {
|
|
||||||
|
|
||||||
var rtnTitle = i18n._('ALL ACTIVITY');
|
var rtnTitle = i18n._('ALL ACTIVITY');
|
||||||
|
|
||||||
switch(target) {
|
switch(target) {
|
||||||
case 'project':
|
case 'project':
|
||||||
rtnTitle = i18n._('PROJECTS');
|
rtnTitle = i18n._('PROJECTS');
|
||||||
break;
|
break;
|
||||||
case 'inventory':
|
case 'inventory':
|
||||||
rtnTitle = i18n._('INVENTORIES');
|
rtnTitle = i18n._('INVENTORIES');
|
||||||
break;
|
break;
|
||||||
case 'credential':
|
case 'credential':
|
||||||
rtnTitle = i18n._('CREDENTIALS');
|
rtnTitle = i18n._('CREDENTIALS');
|
||||||
break;
|
break;
|
||||||
case 'user':
|
case 'user':
|
||||||
rtnTitle = i18n._('USERS');
|
rtnTitle = i18n._('USERS');
|
||||||
break;
|
break;
|
||||||
case 'team':
|
case 'team':
|
||||||
rtnTitle = i18n._('TEAMS');
|
rtnTitle = i18n._('TEAMS');
|
||||||
break;
|
break;
|
||||||
case 'notification_template':
|
case 'notification_template':
|
||||||
rtnTitle = i18n._('NOTIFICATION TEMPLATES');
|
rtnTitle = i18n._('NOTIFICATION TEMPLATES');
|
||||||
break;
|
break;
|
||||||
case 'organization':
|
case 'organization':
|
||||||
rtnTitle = i18n._('ORGANIZATIONS');
|
rtnTitle = i18n._('ORGANIZATIONS');
|
||||||
break;
|
break;
|
||||||
case 'job':
|
case 'job':
|
||||||
rtnTitle = i18n._('JOBS');
|
rtnTitle = i18n._('JOBS');
|
||||||
break;
|
break;
|
||||||
case 'custom_inventory_script':
|
case 'custom_inventory_script':
|
||||||
rtnTitle = i18n._('INVENTORY SCRIPTS');
|
rtnTitle = i18n._('INVENTORY SCRIPTS');
|
||||||
break;
|
break;
|
||||||
case 'schedule':
|
case 'schedule':
|
||||||
rtnTitle = i18n._('SCHEDULES');
|
rtnTitle = i18n._('SCHEDULES');
|
||||||
break;
|
break;
|
||||||
case 'host':
|
case 'host':
|
||||||
rtnTitle = i18n._('HOSTS');
|
rtnTitle = i18n._('HOSTS');
|
||||||
break;
|
break;
|
||||||
case 'template':
|
case 'template':
|
||||||
rtnTitle = i18n._('TEMPLATES');
|
rtnTitle = i18n._('TEMPLATES');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rtnTitle;
|
return rtnTitle;
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
GetTargetTitle.$inject = ['i18n'];
|
GetTargetTitle.$inject = ['i18n'];
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import ShowDetail from './factories/show-detail.factory';
|
|||||||
import Stream from './factories/stream.factory';
|
import Stream from './factories/stream.factory';
|
||||||
import GetTargetTitle from './get-target-title.factory';
|
import GetTargetTitle from './get-target-title.factory';
|
||||||
import ModelToBasePathKey from './model-to-base-path-key.factory';
|
import ModelToBasePathKey from './model-to-base-path-key.factory';
|
||||||
|
import ActivityDetailForm from './activity-detail.form';
|
||||||
|
import StreamList from './streams.list';
|
||||||
|
|
||||||
export default angular.module('activityStream', [streamDetailModal.name])
|
export default angular.module('activityStream', [streamDetailModal.name])
|
||||||
.controller('activityStreamController', activityStreamController)
|
.controller('activityStreamController', activityStreamController)
|
||||||
@@ -24,6 +26,8 @@ export default angular.module('activityStream', [streamDetailModal.name])
|
|||||||
.factory('Stream', Stream)
|
.factory('Stream', Stream)
|
||||||
.factory('GetTargetTitle', GetTargetTitle)
|
.factory('GetTargetTitle', GetTargetTitle)
|
||||||
.factory('ModelToBasePathKey', ModelToBasePathKey)
|
.factory('ModelToBasePathKey', ModelToBasePathKey)
|
||||||
|
.factory('ActivityDetailForm', ActivityDetailForm)
|
||||||
|
.factory('StreamList', StreamList)
|
||||||
.run(['$stateExtender', function($stateExtender) {
|
.run(['$stateExtender', function($stateExtender) {
|
||||||
$stateExtender.addState(activityStreamRoute);
|
$stateExtender.addState(activityStreamRoute);
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -10,50 +10,49 @@
|
|||||||
* @description Helper functions to convert singular/plural versions of our models to the opposite
|
* @description Helper functions to convert singular/plural versions of our models to the opposite
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default
|
export default function ModelToBasePathKey() {
|
||||||
function ModelToBasePathKey() {
|
return function(model) {
|
||||||
return function(model) {
|
// This function takes in the singular model string and returns the key needed
|
||||||
// This function takes in the singular model string and returns the key needed
|
// to get the base path from $rootScope/local storage.
|
||||||
// to get the base path from $rootScope/local storage.
|
|
||||||
|
|
||||||
var basePathKey;
|
var basePathKey;
|
||||||
|
|
||||||
switch(model) {
|
switch(model) {
|
||||||
case 'project':
|
case 'project':
|
||||||
basePathKey = 'projects';
|
basePathKey = 'projects';
|
||||||
break;
|
break;
|
||||||
case 'inventory':
|
case 'inventory':
|
||||||
basePathKey = 'inventory';
|
basePathKey = 'inventory';
|
||||||
break;
|
break;
|
||||||
case 'job_template':
|
case 'job_template':
|
||||||
basePathKey = 'job_templates';
|
basePathKey = 'job_templates';
|
||||||
break;
|
break;
|
||||||
case 'credential':
|
case 'credential':
|
||||||
basePathKey = 'credentials';
|
basePathKey = 'credentials';
|
||||||
break;
|
break;
|
||||||
case 'user':
|
case 'user':
|
||||||
basePathKey = 'users';
|
basePathKey = 'users';
|
||||||
break;
|
break;
|
||||||
case 'team':
|
case 'team':
|
||||||
basePathKey = 'teams';
|
basePathKey = 'teams';
|
||||||
break;
|
break;
|
||||||
case 'notification_template':
|
case 'notification_template':
|
||||||
basePathKey = 'notification_templates';
|
basePathKey = 'notification_templates';
|
||||||
break;
|
break;
|
||||||
case 'organization':
|
case 'organization':
|
||||||
basePathKey = 'organizations';
|
basePathKey = 'organizations';
|
||||||
break;
|
break;
|
||||||
case 'management_job':
|
case 'management_job':
|
||||||
basePathKey = 'management_jobs';
|
basePathKey = 'management_jobs';
|
||||||
break;
|
break;
|
||||||
case 'custom_inventory_script':
|
case 'custom_inventory_script':
|
||||||
basePathKey = 'inventory_scripts';
|
basePathKey = 'inventory_scripts';
|
||||||
break;
|
break;
|
||||||
case 'workflow_job_template':
|
case 'workflow_job_template':
|
||||||
basePathKey = 'workflow_job_templates';
|
basePathKey = 'workflow_job_templates';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return basePathKey;
|
return basePathKey;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
|
|
||||||
export default
|
export default ['i18n', function(i18n) {
|
||||||
angular.module('StreamListDefinition', [])
|
|
||||||
.factory('StreamList', ['i18n', function(i18n) {
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
name: 'activities',
|
name: 'activities',
|
||||||
@@ -72,4 +70,4 @@ export default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};}]);
|
};}];
|
||||||
@@ -143,8 +143,6 @@ var tower = angular.module('Tower', [
|
|||||||
'AllJobsDefinition',
|
'AllJobsDefinition',
|
||||||
'JobSummaryDefinition',
|
'JobSummaryDefinition',
|
||||||
'HostGroupsFormDefinition',
|
'HostGroupsFormDefinition',
|
||||||
'StreamListDefinition',
|
|
||||||
'ActivityDetailDefinition',
|
|
||||||
'ScheduledJobsDefinition',
|
'ScheduledJobsDefinition',
|
||||||
'JobsListDefinition',
|
'JobsListDefinition',
|
||||||
'LogViewerStatusDefinition',
|
'LogViewerStatusDefinition',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
* All Rights Reserved
|
* All Rights Reserved
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
import ActivityDetail from "./forms/ActivityDetail";
|
|
||||||
import EventsViewer from "./forms/EventsViewer";
|
import EventsViewer from "./forms/EventsViewer";
|
||||||
import Groups from "./forms/Groups";
|
import Groups from "./forms/Groups";
|
||||||
import HostGroups from "./forms/HostGroups";
|
import HostGroups from "./forms/HostGroups";
|
||||||
@@ -23,8 +22,7 @@ import Workflows from "./forms/Workflows";
|
|||||||
|
|
||||||
|
|
||||||
export
|
export
|
||||||
{ ActivityDetail,
|
{ EventsViewer,
|
||||||
EventsViewer,
|
|
||||||
Groups,
|
Groups,
|
||||||
HostGroups,
|
HostGroups,
|
||||||
Hosts,
|
Hosts,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import Organizations from "./lists/Organizations";
|
|||||||
import PortalJobTemplates from "./lists/PortalJobTemplates";
|
import PortalJobTemplates from "./lists/PortalJobTemplates";
|
||||||
import PortalJobs from "./lists/PortalJobs";
|
import PortalJobs from "./lists/PortalJobs";
|
||||||
import ScheduledJobs from "./lists/ScheduledJobs";
|
import ScheduledJobs from "./lists/ScheduledJobs";
|
||||||
import Streams from "./lists/Streams";
|
|
||||||
import Templates from "./lists/Templates";
|
import Templates from "./lists/Templates";
|
||||||
|
|
||||||
export
|
export
|
||||||
@@ -38,6 +37,5 @@ export
|
|||||||
PortalJobTemplates,
|
PortalJobTemplates,
|
||||||
PortalJobs,
|
PortalJobs,
|
||||||
ScheduledJobs,
|
ScheduledJobs,
|
||||||
Streams,
|
|
||||||
Templates
|
Templates
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user