diff --git a/awx/ui/static/js/controllers/Inventories.js b/awx/ui/static/js/controllers/Inventories.js index 6429cff4bf..bfa9589fb3 100644 --- a/awx/ui/static/js/controllers/Inventories.js +++ b/awx/ui/static/js/controllers/Inventories.js @@ -12,9 +12,9 @@ function InventoriesList($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, InventoryList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, Wait, Stream, - EditInventoryProperties) { + EditInventoryProperties, Find) { - ClearScope(); + //ClearScope(); var list = InventoryList, defaultUrl = GetBasePath('inventory'), @@ -82,64 +82,74 @@ function InventoriesList($scope, $rootScope, $location, $log, $routeParams, Rest LoadBreadCrumbs(); + if ($scope.removeBuildPopover) { + $scope.removeBuildPopover(); + } + $scope.removeBuildPopover = $scope.$on('BuildPopover', function(e, data) { + var inventory, html = ''; + if (data.count) { + inventory = Find({ list: $scope.inventories, key: 'id', val: data.results[0].inventory }); + html += "" + + "" + + "" + + "" + + ""; + data.results.forEach(function(row) { + html += "" + + "" + + "" + + ""; + }); + html += "
GroupSourceLast RunStatus
" + row.summary_fields.group.name + "" + row.source + "" + row.last_update + "
\n"; + html += "
esc or click to close
\n"; + inventory.syncPopOver = "bob was here!"; //html; + } + }); + if ($scope.removePostRefresh) { $scope.removePostRefresh(); } $scope.removePostRefresh = $scope.$on('PostRefresh', function () { //If we got here by deleting an inventory, stop the spinner and cleanup events Wait('stop'); - $('#prompt-modal').modal('hide'); - - for (var i = 0; i < $scope.inventories.length; i++) { - - // Set values for Failed Hosts column - // $scope.inventories[i].failed_hosts = $scope.inventories[i].hosts_with_active_failures + ' / ' + $scope.inventories[i].total_hosts; - - if ($scope.inventories[i].hosts_with_active_failures > 0) { - $scope.inventories[i].failed_hosts_tip = $scope.inventories[i].hosts_with_active_failures + - (( $scope.inventories[i].hosts_with_active_failures === 1) ? ' host' : ' hosts') + ' with job failures. Click to view details.'; - $scope.inventories[i].failed_hosts_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].failed_hosts_class = 'true'; - } else { - if ($scope.inventories[i].total_hosts === 0) { - // no hosts - $scope.inventories[i].failed_hosts_tip = "No hosts defined. Click to add."; - $scope.inventories[i].failed_hosts_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].failed_hosts_class = 'na'; - } else { - // many hosts with 0 failures - $scope.inventories[i].failed_hosts_tip = $scope.inventories[i].total_hosts + - (($scope.inventories[i].total_hosts > 1) ? ' hosts' : ' host') + " with no job failures. Click to view details."; - $scope.inventories[i].failed_hosts_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].failed_hosts_class = 'false'; - } - } - - // Set values for Status column - $scope.inventories[i].status = $scope.inventories[i].inventory_sources_with_failures + ' / ' + $scope.inventories[i].total_inventory_sources; - if ($scope.inventories[i].inventory_sources_with_failures > 0) { - $scope.inventories[i].status_tip = $scope.inventories[i].inventory_sources_with_failures + ' cloud ' + - (($scope.inventories[i].inventory_sources_with_failures === 1) ? 'source' : 'sources') + - ' with failures. Click to view details.'; - $scope.inventories[i].status_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].status_class = 'failed'; - } else { - if ($scope.inventories[i].total_inventory_sources === 0) { - // no groups are reporting a source - $scope.inventories[i].status_tip = "Not synced with a cloud source. Click to edit."; - $scope.inventories[i].status_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].status_class = 'na'; - } else { - // many hosts with 0 failures - $scope.inventories[i].status_tip = $scope.inventories[i].total_inventory_sources + - ' cloud ' + (( $scope.inventories[i].total_inventory_sources > 1) ? 'sources' : 'source') + - ' with no failures. Click to view details.'; - $scope.inventories[i].status_link = '/#/inventories/' + $scope.inventories[i].id + '/'; - $scope.inventories[i].status_class = 'successful'; - } - } - + try { + $('#prompt-modal').modal('hide'); } + catch(e) { + // ignore + } + $scope.inventories.forEach(function(inventory, idx) { + if (inventory.has_inventory_sources) { + if (inventory.inventory_sources_with_failures > 0) { + $scope.inventories[idx].syncStatus = 'error'; + $scope.inventories[idx].syncTip = inventory.groups_with_active_failures + ' groups with sync failures. Click for details'; + } + else { + $scope.inventories[idx].syncStatus = 'successful'; + $scope.inventories[idx].syncTip = 'No inventory sync failures. Click for details.'; + } + } + else { + $scope.inventories[idx].syncStatus = 'na'; + $scope.inventories[idx].syncTip = 'Not configured for inventory sync.'; + } + if (inventory.has_active_failures) { + $scope.inventories[idx].hostsStatus = 'error'; + $scope.inventories[idx].hostsTip = inventory.hosts_with_active_failures + ' hosts with failures. Click for details.'; + } + else { + $scope.inventories[idx].hostsStatus = 'successful'; + $scope.inventories[idx].hostsTip = 'No hosts with failures. Click for details.'; + } + + if (inventory.has_inventory_sources) { + Rest.setUrl(inventory.related.inventory_sources + '?or__source=ec2&or__source=rax&order_by=-last_job_run&page_size=5'); + Rest.get() + .success( function(data) { + $scope.$emit('BuildPopover', data); + }); + } + }); }); if ($scope.removeRefreshInventories) { @@ -215,7 +225,7 @@ function InventoriesList($scope, $rootScope, $location, $log, $routeParams, Rest InventoriesList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'InventoryList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', - 'GetBasePath', 'Wait', 'Stream', 'EditInventoryProperties' + 'GetBasePath', 'Wait', 'Stream', 'EditInventoryProperties', 'Find' ]; diff --git a/awx/ui/static/js/helpers/Groups.js b/awx/ui/static/js/helpers/Groups.js index 7f5c91ae6a..445ddf735b 100644 --- a/awx/ui/static/js/helpers/Groups.js +++ b/awx/ui/static/js/helpers/Groups.js @@ -67,8 +67,7 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G if (group) { if (Empty(group.source)) { - Alert('Missing Configuration', 'The selected group is not configured for inventory sync. ' + - 'You must first edit the group, provide Source settings, and then run the sync process.', 'alert-info'); + // do nothing } else if (Empty(group.status) || group.status === "never updated") { Alert('No Status Available', 'An inventory sync has not been performed for the selected group. Start the process by ' + 'clicking the button.', 'alert-info'); @@ -104,18 +103,18 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G if (active_failures > 0) { tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; - html_class = 'true'; + html_class = 'error'; failures = true; } else { failures = false; if (total_hosts === 0) { // no hosts tip = "Group contains 0 hosts."; - html_class = 'na'; + html_class = 'none'; } else { // many hosts with 0 failures tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; - html_class = 'false'; + html_class = 'success'; } } @@ -128,49 +127,59 @@ angular.module('GroupsHelper', ['RestServices', 'Utilities', 'ListGenerator', 'G } ]) -.factory('GetSyncStatusMsg', [ - function () { +.factory('GetSyncStatusMsg', [ 'Empty', + function (Empty) { return function (params) { var status = params.status, + source = params.source, + has_inventory_sources = params.has_inventory_sources, launch_class = '', launch_tip = 'Start sync process', + stat, stat_class, status_tip; stat = status; - stat_class = 'icon-cloud-' + stat; + stat_class = stat; switch (status) { - case 'never updated': - stat = 'never'; - stat_class = 'icon-cloud-na disabled'; - status_tip = 'Sync not performed. Click to start it now.'; - break; - case 'none': - case '': - launch_class = 'btn-disabled'; - stat = 'n/a'; - stat_class = 'icon-cloud-na disabled'; - status_tip = 'Cloud source not configured. Click to update.'; - launch_tip = status_tip; - break; - case 'failed': - status_tip = 'Sync failed. Click to view log.'; - break; - case 'successful': - status_tip = 'Sync completed. Click to view log.'; - break; - case 'updating': - status_tip = 'Sync running'; - break; + case 'never updated': + stat = 'never'; + stat_class = 'na'; + status_tip = 'Sync not performed. Click to start it now.'; + break; + case 'none': + case '': + launch_class = 'btn-disabled'; + stat = 'n/a'; + stat_class = 'na'; + status_tip = 'Cloud source not configured. Click to update.'; + launch_tip = 'Cloud source not configured.'; + break; + case 'failed': + status_tip = 'Sync failed. Click to view log.'; + break; + case 'successful': + status_tip = 'Sync completed. Click to view log.'; + break; + case 'updating': + status_tip = 'Sync running'; + break; + } + + if (has_inventory_sources && Empty(source)) { + // parent has a source, therefore this group should not have a source + launch_class = "btn-disabled"; + status_tip = 'Managed by an external cloud source.'; + launch_tip = 'Can only be updated by running a sync on the parent group.'; } return { - 'class': stat_class, - tooltip: status_tip, - status: stat, - 'launch_class': launch_class, - 'launch_tip': launch_tip + "class": stat_class, + "tooltip": status_tip, + "status": stat, + "launch_class": launch_class, + "launch_tip": launch_tip }; }; } diff --git a/awx/ui/static/js/helpers/Hosts.js b/awx/ui/static/js/helpers/Hosts.js index d8a49ebf7d..db21033194 100644 --- a/awx/ui/static/js/helpers/Hosts.js +++ b/awx/ui/static/js/helpers/Hosts.js @@ -91,10 +91,10 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'H html = "\n"; html += "\n"; html += "\n"; - html += "\n"; + html += "\n"; html += "\n"; - html += "\n"; html += "\n"; html += "\n"; html += "\n"; diff --git a/awx/ui/static/js/lists/CompletedJobs.js b/awx/ui/static/js/lists/CompletedJobs.js index c5aea4470e..df2d6fca8a 100644 --- a/awx/ui/static/js/lists/CompletedJobs.js +++ b/awx/ui/static/js/lists/CompletedJobs.js @@ -52,7 +52,7 @@ angular.module('CompletedJobsDefinition', []) }, finished: { label: 'Finished On', - link: false, + noLink: true, searchable: false, filter: "date:'MM/dd/yy HH:mm:ss'", columnClass: "col-md-2 hidden-xs", diff --git a/awx/ui/static/js/lists/Inventories.js b/awx/ui/static/js/lists/Inventories.js index a30d8c0f22..b3a50725d9 100644 --- a/awx/ui/static/js/lists/Inventories.js +++ b/awx/ui/static/js/lists/Inventories.js @@ -21,6 +21,27 @@ angular.module('InventoriesListDefinition', []) hover: true, fields: { + status: { + label: 'Status', + columnClass: 'col-md-2 col-sm-2 col-xs-2', + searchable: false, + nosort: true, + ngClick: "null", + dataTitle: "Sync Status", + icons: [{ + icon: "{{ 'icon-cloud-' + inventory.syncStatus }}", + awToolTip: "{{ inventory.syncTip }}", + awTipPlacement: "top", + awPopOver: "{{ inventory.syncPopOver }}", + dataPlacement: "right" + },{ + icon: "{{ 'icon-job-' + inventory.hostsStatus }}", + awToolTip: "{{ inventory.hostsTip }}", + awTipPlacement: "top", + awPopOver: "{{ inventory.hostsPopOver }}", + dataPlacement: "right" + }] + }, name: { key: true, label: 'Name' @@ -70,13 +91,6 @@ angular.module('InventoriesListDefinition', []) }, fieldActions: { - status: { - //label: 'Status', - ngHref: "{{ inventory.status_link }}", - iconClass: "{{ 'fa fa-cloud icon-cloud-' + inventory.status_class }}", - awToolTip: "{{ inventory.status_tip }}", - dataPlacement: "top" - }, failed_hosts: { //label: 'Failures', ngHref: "{{ inventory.failed_hosts_link }}", diff --git a/awx/ui/static/js/lists/InventoryGroups.js b/awx/ui/static/js/lists/InventoryGroups.js index cf8691f1b3..a41084db04 100644 --- a/awx/ui/static/js/lists/InventoryGroups.js +++ b/awx/ui/static/js/lists/InventoryGroups.js @@ -76,7 +76,8 @@ angular.module('InventoryGroupsDefinition', []) ngShow: "group.id > 1", // hide for all hosts awToolTip: "{{ group.status_tooltip }}", dataTipWatch: "group.launch_tooltip", - ngClass: "group.status_class", + iconClass: "{{ 'fa icon-cloud-' + group.status_class }}", + ngClass: "group.launch_class", dataPlacement: "top" }, failed_hosts: { @@ -85,7 +86,7 @@ angular.module('InventoryGroupsDefinition', []) ngShow: "group.id > 1", // hide for all hosts dataPlacement: "top", ngClick: "showHosts(group.id, group.group_id, group.show_failures)", - iconClass: "{{ 'fa icon-failures-' + group.hosts_status_class }}" + iconClass: "{{ 'fa icon-job-' + group.hosts_status_class }}" }, group_update: { //label: 'Sync', diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index f45e06fdef..cdbd190706 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1078,18 +1078,20 @@ input[type="checkbox"].checkbox-no-label { /* Cloud inventory status. i.e. inventory_source.status values */ - /* .icon-cloud-na:before, .icon-cloud-never:before, .icon-cloud-updating:before, - .icon-cloud-successful:before { - content: "\f111"; + .icon-cloud-running:before, + .icon-cloud-successful:before, + .icon-cloud-failed:before, + .icon-cloud-error:before { + content: "\f0c2"; } - .icon-cloud-failed:before { + /*.icon-cloud-failed:before, + .icon-cloud-error:before { content: "\f06a"; - } - */ + }*/ .icon-cloud-na, .icon-cloud-never, @@ -1099,6 +1101,7 @@ input[type="checkbox"].checkbox-no-label { } .icon-cloud-updating, + .icon-cloud-running, .icon-cloud-successful, a.icon-cloud-updating:hover, a.icon-cloud-successful:hover { @@ -1106,11 +1109,13 @@ input[type="checkbox"].checkbox-no-label { } .icon-cloud-failed, + .icon-cloud-error, a.icon-cloud-failed:hover { color: @red; } - .icon-cloud-updating { + .icon-cloud-updating, + .icon-cloud-running { .pulsate(); } @@ -1244,6 +1249,10 @@ input[type="checkbox"].checkbox-no-label { /* Inventory Edit */ + #inventories_table i[class*="icon-job-"] { + margin-left: 8px; + } + .selected { font-weight: bold; color: @blue-dark; diff --git a/awx/ui/static/lib/ansible/InventoryTree.js b/awx/ui/static/lib/ansible/InventoryTree.js index 52f60dbbe3..ac9a8173e8 100644 --- a/awx/ui/static/lib/ansible/InventoryTree.js +++ b/awx/ui/static/lib/ansible/InventoryTree.js @@ -76,13 +76,16 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P function buildGroups(tree_data, parent, level) { - var i, j, children, stat, hosts_status, group, + var children, stat, hosts_status, group, sorted = SortNodes(tree_data); - for (i = 0; i < sorted.length; i++) { + sorted.forEach( function(row, i) { id++; + stat = GetSyncStatusMsg({ - status: sorted[i].summary_fields.inventory_source.status + status: sorted[i].summary_fields.inventory_source.status, + has_inventory_sources: sorted[i].has_inventory_sources, + source: ( (sorted[i].summary_fields.inventory_source) ? sorted[i].summary_fields.inventory_source.source : null ) }); // from helpers/Groups.js hosts_status = GetHostsStatusMsg({ @@ -93,9 +96,9 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P }); // from helpers/Groups.js children = []; - for (j = 0; j < sorted[i].children.length; j++) { + sorted[i].children.forEach( function(child, j) { children.push(sorted[i].children[j].id); - } + }); group = { name: sorted[i].name, @@ -140,7 +143,7 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P if (sorted[i].children.length > 0) { buildGroups(sorted[i].children, id, level + 1); } - } + }); } // Build the HTML for our tree @@ -221,7 +224,9 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P } // Update date sync status links/icons stat = GetSyncStatusMsg({ - status: scope.groups[i].status + status: scope.groups[i].status, + has_inventory_sources: scope.groups[i].has_inventory_sources, + source: scope.groups[i].source }); scope.groups[i].status_class = stat['class']; scope.groups[i].status_tooltip = stat.tooltip; diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index fc199d2624..45b92c7812 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -308,18 +308,19 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job } } }); + /* $('.popover').each(function() { // remove lingering popover
. Seems to be a bug in TB3 RC1 $(this).remove(); - }); - $('.tooltip').each( function() { + });*/ + /*$('.tooltip').each( function() { // close any lingering tool tipss $(this).hide(); - }); + });*/ $(this).popover('toggle'); - $('.popover').each(function() { + /*$('.popover').each(function() { $compile($(this))(scope); //make nested directives work! - }); + });*/ $('.popover-content, .popover-title').click(function() { $(self).popover('hide'); }); diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index f2c38f1d9a..bc82b8dfa8 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -441,15 +441,90 @@ angular.module('GeneratorHelpers', []) } ]) -.factory('Column', ['Attr', 'Icon', 'DropDown', 'Badge', 'BadgeCount', - function (Attr, Icon, DropDown, Badge, BadgeCount) { +// List field with multiple icons +.factory('BuildLink', ['Attr', 'Icon', function(Attr, Icon){ + return function(params) { + var html = '', + field = params.field, + list = params.list, + base = params.base, + fld = params.fld; + + if (field.linkTo) { + html += ""; + } + if (field.awDroppable) { + html += Attr(field, 'awDroppable'); + html += (field.dataAccept) ? Attr(field, 'dataAccept') : ''; + } + if (field.awDraggable) { + html += Attr(field, 'awDraggable'); + html += (field.dataContainment) ? Attr(field, 'dataContainment') : ''; + html += (field.dataTreeId) ? Attr(field, 'dataTreeId') : ''; + html += (field.dataGroupId) ? Attr(field, 'dataGroupId') : ''; + html += (field.dataHostId) ? Attr(field, 'dataHostId') : ''; + html += (field.dataType) ? Attr(field, 'dataType') : ''; + } + if (field.awToolTip) { + html += Attr(field, 'awToolTip'); + html += (field.dataPlacement && !field.awPopOver) ? Attr(field, 'dataPlacement') : ""; + html += (field.dataTipWatch) ? Attr(field, 'dataTipWatch') : ""; + html += (field.awTipPlacement) ? Attr(field, 'awTipPlacement') : ""; + } + if (field.awPopOver) { + html += "aw-pop-over=\"" + field.awPopOver + "\" "; + html += (field.dataPlacement) ? "data-placement=\"" + field.dataPlacement + "\" " : ""; + } + html += ">"; + + // Add icon: + if (field.ngShowIcon) { + html += " "; + } else if (field.icon) { + html += Icon(field.icon) + " "; + } + + // Add data binds + if (!field.ngBindHtml && !field.iconOnly && (field.showValue === undefined || field.showValue === true)) { + if (field.ngBind) { + html += "{{ " + field.ngBind; + } else { + html += "{{" + list.iterator + "." + fld; + } + if (field.filter) { + html += " | " + field.filter + " }}"; + } + else { + html += " }}"; + } + } + + // Add additional text: + if (field.text) { + html += field.text; + } + html += ""; + return html; + }; +}]) + +.factory('Column', ['Attr', 'Icon', 'DropDown', 'Badge', 'BadgeCount', 'BuildLink', + function (Attr, Icon, DropDown, Badge, BadgeCount, BuildLink) { return function (params) { var list = params.list, fld = params.fld, options = params.options, base = params.base, field = list.fields[fld], - cap, html = ''; + html = ''; if (field.type !== undefined && field.type === 'DropDown') { html = DropDown(params); @@ -483,7 +558,6 @@ angular.module('GeneratorHelpers', []) html += "
" + "
"; - //ng-show=\"'\{\{ " + list.iterator + ".related.children \}\}' !== ''\" } if (list.name === 'groups') { @@ -496,84 +570,54 @@ angular.module('GeneratorHelpers', []) // Start the Link if ((field.key || field.link || field.linkTo || field.ngClick || field.ngHref || field.awToolTip || field.awPopOver) && options.mode !== 'lookup' && options.mode !== 'select' && !field.noLink && !field.ngBindHtml) { - cap = false; - if (field.linkTo) { - html += " "; - } else { - if (field.icon) { - html += Icon(field.icon) + " "; - } - } - - // Add data binds - if (!field.ngBindHtml && !field.iconOnly && (field.showValue === undefined || field.showValue === true)) { - if (field.ngBind) { - html += "{{ " + field.ngBind; - } else { - html += "{{" + list.iterator + "." + fld; - } - if (field.filter) { - html += " | " + field.filter + " }}"; + if (field.icons) { + field.icons.forEach(function(icon, idx) { + var key, i = field.icons[idx]; + for (key in i) { + field[key] = i[key]; + } + html += BuildLink({ + list: list, + field: field, + fld: fld, + base: base + }) + ' '; + }); } else { - html += " }}"; + html += BuildLink({ + list: list, + field: field, + fld: fld, + base: base + }); } } - - // Add additional text: - if (field.text) { - html += field.text; - } - - //if (list['hasChildren'] && field.hasChildren) { - // html += ""; - //} - - // close the link - if ((field.key || field.link || field.linkTo || field.ngClick || field.ngHref || field.awToolTip || field.awPopOver) && - options.mode !== 'lookup' && options.mode !== 'select' && !field.noLink && !field.ngBindHtml) { - html += ""; + else { + // Add icon: + if (field.ngShowIcon) { + html += " "; + } else if (field.icon) { + html += Icon(field.icon) + " "; + } + // Add data binds + if (!field.ngBindHtml && !field.iconOnly && (field.showValue === undefined || field.showValue === true)) { + if (field.ngBind) { + html += "{{ " + field.ngBind; + } else { + html += "{{" + list.iterator + "." + fld; + } + if (field.filter) { + html += " | " + field.filter + " }}"; + } + else { + html += " }}"; + } + } + // Add additional text: + if (field.text) { + html += field.text; + } } if (list.name === 'hosts' || list.name === 'groups') {
ID\n"; - html += "Status\n"; + html += "IDStatusViewName\n"; + html += "Name