/********************************************* * Copyright (c) 2014 AnsibleWorks, Inc. * * GroupsHelper * * Routines that handle group add/edit/delete on the Inventory tree widget. * */ 'use strict'; angular.module('GroupsHelper', [ 'RestServices', 'Utilities', 'ListGenerator', 'GroupListDefinition', 'SearchHelper', 'PaginationHelpers', 'ListGenerator', 'AuthService', 'GroupsHelper', 'InventoryHelper', 'SelectionHelper', 'JobSubmissionHelper', 'RefreshHelper', 'PromptDialog', 'CredentialsListDefinition', 'InventoryTree', 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'SourceFormDefinition', 'LogViewerHelper', 'SchedulesHelper' ]) .factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath', function (Rest, ProcessErrors, GetBasePath) { return function (params) { // Lookup options for source and build an array of drop-down choices var scope = params.scope, variable = params.variable; if (scope[variable] === undefined) { scope[variable] = []; Rest.setUrl(GetBasePath('inventory_sources')); Rest.options() .success(function (data) { var i, choices = data.actions.GET.source.choices; for (i = 0; i < choices.length; i++) { if (choices[i][0] !== 'file') { scope[variable].push({ label: ((choices[i][0] === '') ? 'Manual' : choices[i][1]), value: choices[i][0] }); } } scope.$emit('sourceTypeOptionsReady'); }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status }); }); } }; } ]) .factory('ViewUpdateStatus', ['Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find', 'LogViewer', function (Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find, LogViewer) { return function (params) { var scope = params.scope, tree_id = params.tree_id, group = Find({ list: scope.groups, key: 'id', val: tree_id }); if (scope.removeSourceReady) { scope.removeSourceReady(); } scope.removeSourceReady = scope.$on('SourceReady', function(e, url) { LogViewer({ scope: scope, url: url }); }); if (group) { if (Empty(group.source)) { // 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'); } else { Wait('start'); Rest.setUrl(group.related.inventory_source); Rest.get() .success(function (data) { var url = (data.related.current_update) ? data.related.current_update : data.related.last_update; scope.$emit('SourceReady', url); }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve inventory source: ' + group.related.inventory_source + ' POST returned status: ' + status }); }); } } }; } ]) .factory('GetHostsStatusMsg', [ function () { return function (params) { var active_failures = params.active_failures, total_hosts = params.total_hosts, tip, failures, html_class; // Return values for use on host status indicator if (active_failures > 0) { tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. ' + active_failures + ' with failed jobs.'; html_class = 'error'; failures = true; } else { failures = false; if (total_hosts === 0) { // no hosts tip = "Group contains 0 hosts."; html_class = 'none'; } else { // many hosts with 0 failures tip = total_hosts + ((total_hosts === 1) ? ' host' : ' hosts') + '. No job failures'; html_class = 'success'; } } return { tooltip: tip, failures: failures, 'class': html_class }; }; } ]) .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 = stat; switch (status) { 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.'; } if (has_inventory_sources === false && Empty(source)) { launch_class = 'btn-disabled'; status_tip = 'Cloud source not configured. Click to update.'; launch_tip = 'Cloud source not configured.'; } return { "class": stat_class, "tooltip": status_tip, "status": stat, "launch_class": launch_class, "launch_tip": launch_tip }; }; } ]) .factory('SourceChange', ['GetBasePath', 'CredentialList', 'LookUpInit', 'Empty', 'Wait', 'ParseTypeChange', function (GetBasePath, CredentialList, LookUpInit, Empty, Wait, ParseTypeChange) { return function (params) { var scope = params.scope, form = params.form, kind, url, callback; if (!Empty(scope.source)) { if (scope.source.value === 'file') { scope.sourcePathRequired = true; } else { scope.sourcePathRequired = false; // reset fields scope.source_path = ''; scope[form.name + '_form'].source_path.$setValidity('required', true); } if (scope.source.value === 'rax') { scope.source_region_choices = scope.rax_regions; //$('#s2id_group_source_regions').select2('data', []); $('#s2id_source_source_regions').select2('data', [{ id: 'all', text: 'All' }]); $('#source_form').removeClass('squeeze'); } else if (scope.source.value === 'ec2') { scope.source_region_choices = scope.ec2_regions; //$('#s2id_group_source_regions').select2('data', []); $('#s2id_source_source_regions').select2('data', [{ id: 'all', text: 'All' }]); $('#source_form').addClass('squeeze'); } if (scope.source.value === 'rax' || scope.source.value === 'ec2') { kind = (scope.source.value === 'rax') ? 'rax' : 'aws'; url = GetBasePath('credentials') + '?cloud=true&kind=' + kind; LookUpInit({ url: url, scope: scope, form: form, list: CredentialList, field: 'credential' }); if ($('#group_tabs .active a').text() === 'Source' && scope.source.value === 'ec2') { callback = function(){ Wait('stop'); }; Wait('start'); scope.source_vars = (Empty(scope.source_vars)) ? "---" : scope.source_vars; ParseTypeChange({ scope: scope, variable: 'source_vars', parse_variable: form.fields.source_vars.parseTypeName, field_id: 'source_source_vars', onReady: callback }); } } } }; } ]) // Cancel a pending or running inventory sync .factory('GroupsCancelUpdate', ['Empty', 'Rest', 'ProcessErrors', 'Alert', 'Wait', 'Find', function (Empty, Rest, ProcessErrors, Alert, Wait, Find) { return function (params) { var scope = params.scope, id = params.id, group = params.group; if (scope.removeCancelUpdate) { scope.removeCancelUpdate(); } scope.removeCancelUpdate = scope.$on('CancelUpdate', function (e, url) { // Cancel the update process Rest.setUrl(url); Rest.post() .success(function () { Wait('stop'); Alert('Inventory Sync Cancelled', 'Request to cancel the sync process was submitted to the task manger. ' + 'Click the button to monitor the status.', 'alert-info'); }) .error(function (data, status) { Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. POST status: ' + status }); }); }); if (scope.removeCheckCancel) { scope.removeCheckCancel(); } scope.removeCheckCancel = scope.$on('CheckCancel', function (e, last_update, current_update) { // Check that we have access to cancelling an update var url = (current_update) ? current_update : last_update; url += 'cancel/'; Rest.setUrl(url); Rest.get() .success(function (data) { if (data.can_cancel) { scope.$emit('CancelUpdate', url); } else { Wait('stop'); Alert('Cancel Inventory Sync', 'The sync process completed. Click the button to view ' + 'the latest status.', 'alert-info'); } }) .error(function (data, status) { Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + ' failed. GET status: ' + status }); }); }); // Cancel the update process if (Empty(group)) { group = Find({ list: scope.groups, key: 'id', val: id }); scope.selected_tree_id = group.id; scope.selected_group_id = group.group_id; } if (group && (group.status === 'running' || group.status === 'pending')) { // We found the group, and there is a running update Wait('start'); Rest.setUrl(group.related.inventory_source); Rest.get() .success(function (data) { scope.$emit('CheckCancel', data.related.last_update, data.related.current_update); }) .error(function (data, status) { Wait('stop'); ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + group.related.inventory_source + ' failed. GET status: ' + status }); }); } else { Alert('Cancel Inventory Sync', 'The sync process completed. Click the to' + ' view the latest status.', 'alert-info'); } }; } ]) /** * * Add the list of schedules to the Group Edit modal * */ .factory('GroupsScheduleListInit', ['GroupsScheduleEdit', 'SchedulesList', 'GenerateList', 'SearchInit', 'PaginateInit', 'Rest', 'PageRangeSetup', 'Wait', 'ProcessErrors', 'Find', 'ToggleSchedule', 'DeleteSchedule', 'GetBasePath', 'SchedulesListInit', function(GroupsScheduleEdit, SchedulesList, GenerateList, SearchInit, PaginateInit, Rest, PageRangeSetup, Wait, ProcessErrors, Find, ToggleSchedule, DeleteSchedule, GetBasePath, SchedulesListInit) { return function(params) { var schedule_scope = params.scope, url = params.url, list; // Clean up $('#schedules-list').hide().empty(); $('#schedules-form-container').hide(); $('#schedules-form').empty(); $('.tooltip').each(function () { $(this).remove(); }); $('.popover').each(function () { $(this).remove(); }); // Add schedules list list = angular.copy(SchedulesList); delete list.fields.dtend; delete list.actions.stream; list.well = false; GenerateList.inject(list, { mode: 'edit', id: 'schedules-list', breadCrumbs: false, searchSize: 'col-lg-6 col-md-5 col-sm-5 col-xs-5', scope: schedule_scope }); $('#schedules-list').show(); // Removing screws up /home/groups page // if (schedule_scope.removePostRefresh) { // schedule_scope.removePostRefresh(); //} schedule_scope.removePostRefresh = schedule_scope.$on('PostRefresh', function() { SchedulesListInit({ scope: schedule_scope, list: list, choices: null }); }); SearchInit({ scope: schedule_scope, set: 'schedules', list: SchedulesList, url: url }); PaginateInit({ scope: schedule_scope, list: SchedulesList, url: url, pageSize: 5 }); schedule_scope.search(list.iterator); schedule_scope.refreshSchedules = function() { schedule_scope.search(list.iterator); }; schedule_scope.editSchedule = function(id) { GroupsScheduleEdit({ scope: schedule_scope, mode: 'edit', url: GetBasePath('schedules') + id + '/' }); }; schedule_scope.addSchedule = function() { GroupsScheduleEdit({ scope: schedule_scope, mode: 'add', url: url }); }; if (schedule_scope.removeSchedulesRefresh) { schedule_scope.removeSchedulesRefresh(); } schedule_scope.removeSchedulesRefresh = schedule_scope.$on('SchedulesRefresh', function() { schedule_scope.search(list.iterator); }); schedule_scope.toggleSchedule = function(event, id) { try { $(event.target).tooltip('hide'); } catch(e) { // ignore } ToggleSchedule({ scope: schedule_scope, id: id, callback: 'SchedulesRefresh' }); }; schedule_scope.deleteSchedule = function(id) { DeleteSchedule({ scope: schedule_scope, id: id, callback: 'SchedulesRefresh' }); }; }; }]) .factory('SetSchedulesInnerDialogSize', [ function() { return function() { var height = $('#group-modal-dialog').outerHeight() - $('#group_tabs').outerHeight() - 25; height = height - 110 - $('#schedules-buttons').outerHeight(); $('#schedules-form-container-body').height(height); }; }]) /** * * Remove the schedule list, add the schedule widget and populate it with an rrule * */ .factory('GroupsScheduleEdit', ['$compile','SchedulerInit', 'Rest', 'Wait', 'SetSchedulesInnerDialogSize', 'SchedulePost', 'ProcessErrors', function($compile, SchedulerInit, Rest, Wait, SetSchedulesInnerDialogSize, SchedulePost, ProcessErrors) { return function(params) { var parent_scope = params.scope, mode = params.mode, // 'add' or 'edit' url = params.url, scope = parent_scope.$new(), schedule = {}, scheduler, target, showForm, list, detail, restoreList, container, elem; Wait('start'); detail = $('#schedules-detail').hide(); list = $('#schedules-list'); target = $('#schedules-form'); container = $('#schedules-form-container'); // Clean up any lingering stuff container.hide(); target.empty(); $('.tooltip').each(function () { $(this).remove(); }); $('.popover').each(function () { $(this).remove(); }); elem = angular.element(document.getElementById('schedules-form-container')); $compile(elem)(scope); if (scope.removeScheduleReady) { scope.removeScheduleReady(); } scope.removeScheduleReady = scope.$on('ScheduleReady', function() { // Insert the scheduler widget into the hidden div scheduler = SchedulerInit({ scope: scope, requireFutureStartTime: false }); scheduler.inject('schedules-form', false); scheduler.injectDetail('schedules-detail', false); scheduler.clear(); scope.formShowing = true; scope.showRRuleDetail = false; scope.schedulesTitle = (mode === 'edit') ? 'Edit Schedule' : 'Create Schedule'; // display the scheduler widget showForm = function() { Wait('stop'); $('#schedules-overlay').width($('#schedules-tab') .width()).height($('#schedules-tab').height()).show(); container.width($('#schedules-tab').width() - 18); SetSchedulesInnerDialogSize(); container.show('slide', { direction: 'left' }, 300); $('#group-save-button').prop('disabled', true); target.show(); if (mode === 'edit') { scope.$apply(function() { scheduler.setRRule(schedule.rrule); scheduler.setName(schedule.name); }); } }; setTimeout(function() { showForm(); }, 1000); }); restoreList = function() { $('#group-save-button').prop('disabled', false); list.show('slide', { direction: 'right' }, 500); $('#schedules-overlay').width($('#schedules-tab').width()).height($('#schedules-tab').height()).hide(); parent_scope.refreshSchedules(); }; scope.showScheduleDetail = function() { if (scope.formShowing) { if (scheduler.isValid()) { detail.width($('#schedules-form').width()).height($('#schedules-form').height()); target.hide(); detail.show(); scope.formShowing = false; } } else { detail.hide(); target.show(); scope.formShowing = true; } }; if (scope.removeScheduleSaved) { scope.removeScheduleSaved(); } scope.removeScheduleSaved = scope.$on('ScheduleSaved', function() { Wait('stop'); container.hide('slide', { direction: 'right' }, 500, restoreList); scope.$destroy(); }); scope.saveScheduleForm = function() { if (scheduler.isValid()) { scope.schedulerIsValid = true; SchedulePost({ scope: scope, url: url, scheduler: scheduler, callback: 'ScheduleSaved', mode: mode, schedule: schedule }); } else { scope.schedulerIsValid = false; } }; scope.cancelScheduleForm = function() { container.hide('slide', { direction: 'right' }, 500, restoreList); scope.$destroy(); }; if (mode === 'edit') { // Get the existing record Rest.setUrl(url); Rest.get() .success(function(data) { schedule = data; if (!/DTSTART/.test(schedule.rrule)) { schedule.rrule += ";DTSTART=" + schedule.dtstart.replace(/\.\d+Z$/,'Z'); } schedule.rrule = schedule.rrule.replace(/ RRULE:/,';'); schedule.rrule = schedule.rrule.replace(/DTSTART:/,'DTSTART='); scope.$emit('ScheduleReady'); }) .error(function(data,status){ ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get: ' + url + ' GET returned: ' + status }); }); } else { scope.$emit('ScheduleReady'); } }; }]) .factory('GroupsEdit', ['$rootScope', '$location', '$log', '$routeParams', '$compile', 'Rest', 'Alert', 'GroupForm', 'GenerateForm', 'Prompt', 'ProcessErrors', 'GetBasePath', 'SetNodeName', 'ParseTypeChange', 'GetSourceTypeOptions', 'InventoryUpdate', 'LookUpInit', 'Empty', 'Wait', 'GetChoices', 'UpdateGroup', 'SourceChange', 'Find','WatchInventoryWindowResize', 'ParseVariableString', 'ToJSON', 'GroupsScheduleListInit', 'SourceForm', 'SetSchedulesInnerDialogSize', 'BuildTree', function ($rootScope, $location, $log, $routeParams, $compile, Rest, Alert, GroupForm, GenerateForm, Prompt, ProcessErrors, GetBasePath, SetNodeName, ParseTypeChange, GetSourceTypeOptions, InventoryUpdate, LookUpInit, Empty, Wait, GetChoices, UpdateGroup, SourceChange, Find, WatchInventoryWindowResize, ParseVariableString, ToJSON, GroupsScheduleListInit, SourceForm, SetSchedulesInnerDialogSize, BuildTree) { return function (params) { var parent_scope = params.scope, group_id = params.group_id, tree_id = params.tree_id, mode = params.mode, // 'add' or 'edit' inventory_id = params.inventory_id, groups_reload = params.groups_reload, generator = GenerateForm, group_created = false, defaultUrl, master = {}, choicesReady, base = $location.path().replace(/^\//, '').split('/')[0], modal_scope = parent_scope.$new(), properties_scope = parent_scope.$new(), sources_scope = parent_scope.$new(), elem, x, y, ww, wh, maxrows, group, schedules_url = ''; if (mode === 'edit') { defaultUrl = GetBasePath('groups') + group_id + '/'; } else { defaultUrl = (group_id !== null) ? GetBasePath('groups') + group_id + '/children/' : GetBasePath('inventory') + inventory_id + '/groups/'; } $('#properties-tab').empty(); $('#sources-tab').empty(); $('#schedules-list').empty(); $('#schedules-form').empty(); $('#schedules-detail').empty(); elem = document.getElementById('group-modal-dialog'); $compile(elem)(modal_scope); generator.inject(GroupForm, { mode: 'edit', id: 'properties-tab', breadCrumbs: false, related: false, scope: properties_scope }); generator.inject(SourceForm, { mode: 'edit', id: 'sources-tab', breadCrumbs: false, related: false, scope: sources_scope }); //generator.reset(); GetSourceTypeOptions({ scope: sources_scope, variable: 'source_type_options' }); sources_scope.source = SourceForm.fields.source['default']; sources_scope.sourcePathRequired = false; sources_scope[SourceForm.fields.source_vars.parseTypeName] = 'yaml'; sources_scope.update_cache_timeout = 0; properties_scope.parseType = 'yaml'; function waitStop() { Wait('stop'); } // Attempt to create the largest textarea field that will fit on the window. Minimum // height is 6 rows, so on short windows you will see vertical scrolling function textareaResize(textareaID) { var textArea, formHeight, model, windowHeight, offset, rows; textArea = $('#' + textareaID); if (properties_scope.codeMirror) { model = textArea.attr('ng-model'); properties_scope[model] = properties_scope.codeMirror.getValue(); properties_scope.codeMirror.destroy(); } textArea.attr('rows', 1); formHeight = $('#group_form').height(); windowHeight = $('#group-modal-dialog').height() - 20; //leave a margin of 20px offset = Math.floor(windowHeight - formHeight); rows = Math.floor(offset / 24); rows = (rows < 6) ? 6 : rows; textArea.attr('rows', rows); while(rows > 6 && $('#group_form').height() > $('#group-modal-dialog').height()) { rows--; textArea.attr('rows', rows); } ParseTypeChange({ scope: properties_scope, field_id: textareaID, onReady: waitStop }); } // Set modal dimensions based on viewport width ww = $(document).width(); wh = $('body').height(); if (ww > 1199) { // desktop x = 675; y = (800 > wh) ? wh - 15 : 800; maxrows = 18; } else if (ww <= 1199 && ww >= 768) { x = 550; y = (770 > wh) ? wh - 15 : 770; maxrows = 12; } else { x = (ww - 20); y = (770 > wh) ? wh - 15 : 770; maxrows = 10; } // Create the modal $('#group-modal-dialog').dialog({ buttons: { 'Cancel': function() { modal_scope.cancelModal(); }, 'Save': function () { modal_scope.saveGroup(); } }, modal: true, width: x, height: y, autoOpen: false, minWidth: 440, title: 'Edit Group', closeOnEscape: false, create: function () { $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-titlebar button').empty().attr({'class': 'close'}).text('x'); $('.ui-dialog[aria-describedby="group-modal-dialog"]').find('.ui-dialog-buttonset button').each(function () { var c, h, i, l; l = $(this).text(); if (l === 'Cancel') { h = "fa-times"; c = "btn btn-default"; i = "group-close-button"; $(this).attr({ 'class': c, 'id': i }).html(" Cancel"); } else if (l === 'Save') { h = "fa-check"; c = "btn btn-primary"; i = "group-save-button"; $(this).attr({ 'class': c, 'id': i }).html(" Save"); } }); }, resizeStop: function () { // for some reason, after resizing dialog the form and fields (the content) doesn't expand to 100% var dialog = $('.ui-dialog[aria-describedby="group-modal-dialog"]'), titleHeight = dialog.find('.ui-dialog-titlebar').outerHeight(), buttonHeight = dialog.find('.ui-dialog-buttonpane').outerHeight(), content = dialog.find('#group-modal-dialog'), w; content.width(dialog.width() - 28); content.css({ height: (dialog.height() - titleHeight - buttonHeight - 10) }); if ($('#group_tabs .active a').text() === 'Properties') { textareaResize('group_variables', properties_scope); } else if ($('#group_tabs .active a').text() === 'Schedule') { w = $('#group_tabs').width() - 18; $('#schedules-overlay').width(w); $('#schedules-form-container').width(w); SetSchedulesInnerDialogSize(); } }, close: function () { // Destroy on close $('.tooltip').each(function () { // Remove any lingering tooltip