mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 05:29:26 -02:30
Merge remote-tracking branch 'tower/release_3.2.2' into devel
This commit is contained in:
@@ -458,7 +458,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border:1px solid @field-border;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.Form-lookupButton:hover {
|
||||
@@ -470,7 +469,6 @@
|
||||
.Form-lookupButton:active,
|
||||
.Form-lookupButton:focus {
|
||||
border: 1px solid @field-border;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
|
||||
@@ -149,7 +149,7 @@ table.ui-datepicker-calendar {
|
||||
|
||||
.ui-widget-content {
|
||||
background-image: none;
|
||||
background-color: @default-secondary-bg;
|
||||
background-color: @default-bg;
|
||||
|
||||
a,
|
||||
a:visited,
|
||||
|
||||
@@ -248,8 +248,7 @@ table, tbody {
|
||||
}
|
||||
|
||||
.List-buttonDefault[disabled] {
|
||||
color: @d7grey;
|
||||
border-color: @d7grey;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.List-searchDropdown {
|
||||
|
||||
@@ -72,7 +72,7 @@ function AtFormController (eventService, strings) {
|
||||
const data = vm.components
|
||||
.filter(component => component.category === 'input')
|
||||
.reduce((values, component) => {
|
||||
if (!component.state._value) {
|
||||
if (component.state._value === undefined) {
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ function AtInputLookupController (baseInputController, $q, $state) {
|
||||
scope.state._touched = true;
|
||||
|
||||
if (scope.state._displayValue === '' && !scope.state._required) {
|
||||
scope.state._value = null;
|
||||
return vm.check({ isValid: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -62,13 +62,20 @@ function requestWithCache (config) {
|
||||
* @yields {boolean} - Indicating a match has been found. If so, the results
|
||||
* are set on the model.
|
||||
*/
|
||||
function search (params, config) {
|
||||
function search (params = {}, config = {}) {
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: this.path,
|
||||
params
|
||||
url: this.path
|
||||
};
|
||||
|
||||
if (typeof params === 'string') {
|
||||
req.url = `?params`;
|
||||
} else if (Array.isArray(params)) {
|
||||
req.url += `?${params.join('&')}`;
|
||||
} else {
|
||||
req.params = params;
|
||||
}
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (!data.count) {
|
||||
|
||||
@@ -205,7 +205,8 @@ export default [
|
||||
scope: $scope.$parent,
|
||||
variable: name,
|
||||
parse_variable: 'parseType',
|
||||
field_id: form.formDef.name + '_' + name
|
||||
field_id: form.formDef.name + '_' + name,
|
||||
readOnly: $scope.$parent.configDataResolve[name] && $scope.$parent.configDataResolve[name].disabled ? true : false
|
||||
});
|
||||
$scope.parseTypeChange('parseType', name);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,14 @@ export default ['Rest', 'Wait',
|
||||
callback: 'loadCredentialKindOptions'
|
||||
});
|
||||
|
||||
$scope.inputs_help_text = _.get(options, 'actions.POST.inputs.help_text', "Specification for credential type inputs");
|
||||
$scope.injectors_help_text = _.get(options, 'actions.POST.injectors.help_text', "Specification for credential type injector");
|
||||
const docs_url = 'https://docs.ansible.com/ansible-tower/latest/html/userguide/credential_types.html#getting-started-with-credential-types';
|
||||
const docs_help_text = `<br><br><a href=${docs_url}>Getting Started with Credential Types</a>`;
|
||||
|
||||
const api_inputs_help_text = _.get(options, 'actions.POST.inputs.help_text', "Specification for credential type inputs.");
|
||||
const api_injectors_help_text = _.get(options, 'actions.POST.injectors.help_text', "Specification for credential type injector.");
|
||||
|
||||
$scope.inputs_help_text = api_inputs_help_text + docs_help_text;
|
||||
$scope.injectors_help_text = api_injectors_help_text + docs_help_text;
|
||||
|
||||
if (!options.actions.POST) {
|
||||
$state.go("^");
|
||||
|
||||
@@ -36,8 +36,14 @@ export default ['Rest', 'Wait',
|
||||
callback: 'choicesReadyCredentialTypes'
|
||||
});
|
||||
|
||||
$scope.inputs_help_text = _.get(options, 'actions.POST.inputs.help_text', "Specification for credential type inputs");
|
||||
$scope.injectors_help_text = _.get(options, 'actions.POST.injectors.help_text', "Specification for credential type injector");
|
||||
const docs_url = 'https://docs.ansible.com/ansible-tower/latest/html/userguide/credential_types.html#getting-started-with-credential-types';
|
||||
const docs_help_text = `<br><br><a href=${docs_url}>Getting Started with Credential Types</a>`;
|
||||
|
||||
const api_inputs_help_text = _.get(options, 'actions.POST.inputs.help_text', "Specification for credential type inputs.");
|
||||
const api_injectors_help_text = _.get(options, 'actions.POST.injectors.help_text', "Specification for credential type injector.");
|
||||
|
||||
$scope.inputs_help_text = api_inputs_help_text + docs_help_text;
|
||||
$scope.injectors_help_text = api_injectors_help_text + docs_help_text;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
|
||||
ProcessErrors, GetBasePath, Wait, $state, $filter, rbacUiControlService, Dataset,
|
||||
credentialType, i18n, Credential, CredentialsStrings) {
|
||||
|
||||
let credential = new Credential();
|
||||
const credential = new Credential();
|
||||
|
||||
var list = CredentialList,
|
||||
defaultUrl = GetBasePath('credentials');
|
||||
@@ -48,9 +48,25 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
|
||||
return;
|
||||
}
|
||||
|
||||
$scope[list.name].forEach(credential => {
|
||||
credential.kind = credentialType.match('id', credential.credential_type).name;
|
||||
});
|
||||
const params = $scope[list.name]
|
||||
.reduce((accumulator, credential) => {
|
||||
accumulator.push(credential.credential_type);
|
||||
|
||||
return accumulator;
|
||||
}, [])
|
||||
.filter((id, i, array) => array.indexOf(id) === i)
|
||||
.map(id => `or__id=${id}`);
|
||||
|
||||
credentialType.search(params)
|
||||
.then(found => {
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope[list.name].forEach(credential => {
|
||||
credential.kind = credentialType.match('id', credential.credential_type).name;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// iterate over the list and add fields like type label, after the
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
'adjustGraphSize',
|
||||
'templateUrl',
|
||||
'i18n',
|
||||
'moment',
|
||||
'jobStatusGraphData',
|
||||
JobStatusGraph
|
||||
];
|
||||
|
||||
function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, graphDataService) {
|
||||
function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, moment, graphDataService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@@ -72,11 +73,11 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, graphDataSe
|
||||
}
|
||||
});
|
||||
|
||||
if(period==="day") {
|
||||
timeFormat="%H:%M";
|
||||
if(period === "day") {
|
||||
timeFormat="H:M";
|
||||
}
|
||||
else {
|
||||
timeFormat = '%m/%d';
|
||||
timeFormat = "MMM D";
|
||||
}
|
||||
graphData.map(function(series) {
|
||||
series.values = series.values.map(function(d) {
|
||||
@@ -93,7 +94,8 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, graphDataSe
|
||||
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
|
||||
.showLegend(false) //Show the legend, allowing users to turn on/off line series.
|
||||
.showYAxis(true) //Show the y-axis
|
||||
.showXAxis(true); //Show the x-axis
|
||||
.showXAxis(true) //Show the x-axis
|
||||
.margin({ right: 32 });
|
||||
|
||||
job_status_chart.interactiveLayer.tooltip.fixedTop(-10); //distance from the top of the chart to tooltip
|
||||
job_status_chart.interactiveLayer.tooltip.distance(-1); //distance from interactive line to tooltip
|
||||
@@ -101,8 +103,15 @@ function JobStatusGraph($window, adjustGraphSize, templateUrl, i18n, graphDataSe
|
||||
job_status_chart.xAxis
|
||||
.axisLabel(i18n._("TIME"))//.showMaxMin(true)
|
||||
.tickFormat(function(d) {
|
||||
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
|
||||
return dx ? d3.time.format(timeFormat)(new Date(Number(dx+'000'))) : '';
|
||||
const dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
|
||||
|
||||
if (!dx) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tickDate = new Date(Number(dx + '000'));
|
||||
|
||||
return moment(tickDate).format(timeFormat);
|
||||
});
|
||||
|
||||
job_status_chart.yAxis //Chart y-axis settings
|
||||
|
||||
@@ -104,12 +104,14 @@ export default ['i18n', function(i18n) {
|
||||
smart_inventory: {
|
||||
mode: 'all',
|
||||
ngClick: "smartInventory()",
|
||||
awToolTip: i18n._("Create a new Smart Inventory from search results."),
|
||||
awToolTip: "{{ smartInventoryButtonTooltip }}",
|
||||
dataTipWatch: 'smartInventoryButtonTooltip',
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('SMART INVENTORY'),
|
||||
ngShow: 'canAdd && (hosts.length > 0 || !(searchTags | isEmpty))',
|
||||
dataPlacement: "top",
|
||||
ngDisabled: '!enableSmartInventoryButton'
|
||||
ngDisabled: '!enableSmartInventoryButton',
|
||||
showTipWhenDisabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
function HostsList($scope, HostsList, $rootScope, GetBasePath,
|
||||
rbacUiControlService, Dataset, $state, $filter, Prompt, Wait,
|
||||
HostsService, SetStatus, canAdd, $transitions) {
|
||||
HostsService, SetStatus, canAdd, $transitions, InventoryHostsStrings, HostsList) {
|
||||
|
||||
let list = HostsList;
|
||||
|
||||
@@ -16,6 +16,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath,
|
||||
function init(){
|
||||
$scope.canAdd = canAdd;
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
|
||||
// Search init
|
||||
$scope.list = list;
|
||||
@@ -37,14 +38,16 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath,
|
||||
if(trans.params('to') && trans.params('to').host_search) {
|
||||
let hasMoreThanDefaultKeys = false;
|
||||
angular.forEach(trans.params('to').host_search, function(value, key) {
|
||||
if(key !== 'order_by' && key !== 'page_size') {
|
||||
if(key !== 'order_by' && key !== 'page_size' && key !== 'page') {
|
||||
hasMoreThanDefaultKeys = true;
|
||||
}
|
||||
});
|
||||
$scope.enableSmartInventoryButton = hasMoreThanDefaultKeys ? true : false;
|
||||
$scope.smartInventoryButtonTooltip = hasMoreThanDefaultKeys ? InventoryHostsStrings.get('smartinventorybutton.ENABLED_INSTRUCTIONS') : InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
else {
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,20 +86,7 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath,
|
||||
};
|
||||
|
||||
$scope.smartInventory = function() {
|
||||
// Gather up search terms and pass them to the add smart inventory form
|
||||
let stateParamsCopy = angular.copy($state.params.host_search);
|
||||
let defaults = _.find($state.$current.path, (step) => {
|
||||
if(step && step.params && step.params.hasOwnProperty(`host_search`)){
|
||||
return step.params.hasOwnProperty(`host_search`);
|
||||
}
|
||||
}).params[`host_search`].config.value;
|
||||
|
||||
// Strip defaults out of the state params copy
|
||||
angular.forEach(Object.keys(defaults), function(value) {
|
||||
delete stateParamsCopy[value];
|
||||
});
|
||||
|
||||
$state.go('inventories.addSmartInventory', {hostfilter: JSON.stringify(stateParamsCopy)});
|
||||
$state.go('inventories.addSmartInventory', {hostfilter: JSON.stringify({"host_filter":`${$state.params.host_search.host_filter}`})});
|
||||
};
|
||||
|
||||
$scope.editInventory = function(host) {
|
||||
@@ -114,5 +104,5 @@ function HostsList($scope, HostsList, $rootScope, GetBasePath,
|
||||
|
||||
export default ['$scope', 'HostsList', '$rootScope', 'GetBasePath',
|
||||
'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait',
|
||||
'HostsService', 'SetStatus', 'canAdd', '$transitions', HostsList
|
||||
'HostsService', 'SetStatus', 'canAdd', '$transitions', 'InventoryHostsStrings', HostsList
|
||||
];
|
||||
|
||||
@@ -141,9 +141,11 @@
|
||||
} else {
|
||||
$state.go($state.current, reloadListStateParams, {reload: true});
|
||||
}
|
||||
$('#group-delete-modal').modal('hide');
|
||||
$('body').removeClass('modal-open');
|
||||
$('.modal-backdrop').remove();
|
||||
setTimeout(function(){
|
||||
$('#group-delete-modal').modal('hide');
|
||||
$('body').removeClass('modal-open');
|
||||
$('.modal-backdrop').remove();
|
||||
}, 1000);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -153,9 +155,11 @@
|
||||
} else {
|
||||
$state.go($state.current, reloadListStateParams, {reload: true});
|
||||
}
|
||||
$('#group-delete-modal').modal('hide');
|
||||
$('body').removeClass('modal-open');
|
||||
$('.modal-backdrop').remove();
|
||||
setTimeout(function(){
|
||||
$('#group-delete-modal').modal('hide');
|
||||
$('body').removeClass('modal-open');
|
||||
$('.modal-backdrop').remove();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="Modal-title ng-binding">
|
||||
<span translate>Delete Group</span>
|
||||
<a href="" id="awp-promote" href=""
|
||||
aw-pop-over="<dl><dt>Delete</dt><dd>Deletes groups and hosts associated with the group being deleted. If a group or host is associated with other groups, it will still exist within those groups. Otherwise, the associated groups and hosts will no longer appear in the inventory.</dd>\n<dt style='margin-top: 5px;'>Promote</dt><dd>Groups and hosts associated with the group being removed will be promoted root level. Note: groups already associated with other groups cannot be promoted.</dd></dl>\n" aw-tool-tip="Click for help"
|
||||
aw-pop-over="<dl><dt>Delete</dt><dd>Deletes groups and hosts associated with the group being deleted. If a group or host is associated with other groups, it will still exist within those groups. Otherwise, the associated groups and hosts will no longer appear in the inventory.</dd><dt style='margin-top: 5px;'>Promote</dt><dd>Groups and hosts associated with the group being removed will be promoted root level. Note: groups already associated with other groups cannot be promoted.</dd></dl>" aw-tool-tip="Click for help"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
data-title="Delete Group"
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="modal-content Modal-content">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-title ng-binding">
|
||||
<translate>Disassociate Group</translate>
|
||||
<translate>Disassociate Group From Group</translate>
|
||||
<a href="" id="awp-promote" href=""
|
||||
aw-pop-over="<dl><dt>Disassociate</dt><dd>Disassociates this group from the currently targeted parent group.</dd></dl>"
|
||||
aw-tool-tip="Click for help"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
data-title="Disassociate Group"
|
||||
data-title="Disassociate Group From Group"
|
||||
class="help-link">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="modal-content Modal-content">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-title ng-binding">
|
||||
<translate>Disassociate Host</translate>
|
||||
<translate>Disassociate Host From Group</translate>
|
||||
<a href="" id="awp-promote" href=""
|
||||
aw-pop-over="<dl><dt>Disassociate</dt><dd>Disassociates this host from the currently targeted parent group.</dd></dl>"
|
||||
aw-tool-tip="Click for help"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
data-title="Disassociate Host"
|
||||
data-title="Disassociate Host From Group"
|
||||
class="help-link">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePath',
|
||||
'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait',
|
||||
'HostsService', 'SetStatus', 'canAdd', 'GroupsService', 'ProcessErrors', 'groupData', 'inventoryData',
|
||||
'HostsService', 'SetStatus', 'canAdd', 'GroupsService', 'ProcessErrors', 'groupData', 'inventoryData', 'InventoryHostsStrings',
|
||||
'$transitions',
|
||||
function($scope, NestedHostsListDefinition, $rootScope, GetBasePath,
|
||||
rbacUiControlService, Dataset, $state, $filter, Prompt, Wait,
|
||||
HostsService, SetStatus, canAdd, GroupsService, ProcessErrors, groupData, inventoryData,
|
||||
HostsService, SetStatus, canAdd, GroupsService, ProcessErrors, groupData, inventoryData, InventoryHostsStrings,
|
||||
$transitions) {
|
||||
|
||||
let list = NestedHostsListDefinition;
|
||||
@@ -21,6 +21,7 @@ export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePat
|
||||
$scope.canAdd = canAdd;
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.disassociateFrom = groupData;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
|
||||
// Search init
|
||||
$scope.list = list;
|
||||
@@ -52,14 +53,16 @@ export default ['$scope', 'NestedHostsListDefinition', '$rootScope', 'GetBasePat
|
||||
if(trans.params('to') && trans.params('to').host_search) {
|
||||
let hasMoreThanDefaultKeys = false;
|
||||
angular.forEach(trans.params('to').host_search, function(value, key) {
|
||||
if(key !== 'order_by' && key !== 'page_size') {
|
||||
if(key !== 'order_by' && key !== 'page_size' && key !== 'page') {
|
||||
hasMoreThanDefaultKeys = true;
|
||||
}
|
||||
});
|
||||
$scope.enableSmartInventoryButton = hasMoreThanDefaultKeys ? true : false;
|
||||
$scope.smartInventoryButtonTooltip = hasMoreThanDefaultKeys ? InventoryHostsStrings.get('smartinventorybutton.ENABLED_INSTRUCTIONS') : InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
else {
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
// import HostsService from './../hosts/host.service';
|
||||
export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
|
||||
'rbacUiControlService', 'Dataset', '$state', '$filter', 'Prompt', 'Wait',
|
||||
'HostsService', 'SetStatus', 'canAdd', 'i18n', '$transitions',
|
||||
'HostsService', 'SetStatus', 'canAdd', 'i18n', 'InventoryHostsStrings', '$transitions',
|
||||
function($scope, ListDefinition, $rootScope, GetBasePath,
|
||||
rbacUiControlService, Dataset, $state, $filter, Prompt, Wait,
|
||||
HostsService, SetStatus, canAdd, i18n, $transitions) {
|
||||
HostsService, SetStatus, canAdd, i18n, InventoryHostsStrings, $transitions) {
|
||||
|
||||
let list = ListDefinition;
|
||||
|
||||
@@ -19,6 +19,7 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
|
||||
function init(){
|
||||
$scope.canAdd = canAdd;
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
|
||||
// Search init
|
||||
$scope.list = list;
|
||||
@@ -45,14 +46,16 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
|
||||
if(trans.params('to') && trans.params('to').host_search) {
|
||||
let hasMoreThanDefaultKeys = false;
|
||||
angular.forEach(trans.params('to').host_search, function(value, key) {
|
||||
if(key !== 'order_by' && key !== 'page_size') {
|
||||
if(key !== 'order_by' && key !== 'page_size' && key !== 'page') {
|
||||
hasMoreThanDefaultKeys = true;
|
||||
}
|
||||
});
|
||||
$scope.enableSmartInventoryButton = hasMoreThanDefaultKeys ? true : false;
|
||||
$scope.smartInventoryButtonTooltip = hasMoreThanDefaultKeys ? InventoryHostsStrings.get('smartinventorybutton.ENABLED_INSTRUCTIONS') : InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
else {
|
||||
$scope.enableSmartInventoryButton = false;
|
||||
$scope.smartInventoryButtonTooltip = InventoryHostsStrings.get('smartinventorybutton.DISABLED_INSTRUCTIONS');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,7 +92,12 @@ export default ['$scope', 'ListDefinition', '$rootScope', 'GetBasePath',
|
||||
$state.go('inventories.edit.hosts.add');
|
||||
};
|
||||
$scope.editHost = function(host){
|
||||
$state.go('.edit', {inventory_id: host.inventory_id, host_id: host.id});
|
||||
if($state.includes('inventories.edit.hosts')) {
|
||||
$state.go('inventories.edit.hosts.edit', {host_id: host.id});
|
||||
}
|
||||
else if($state.includes('inventories.editSmartInventory.hosts')) {
|
||||
$state.go('inventories.editSmartInventory.hosts.edit', {host_id: host.id});
|
||||
}
|
||||
};
|
||||
$scope.goToInsights = function(host){
|
||||
$state.go('inventories.edit.hosts.edit.insights', {inventory_id: host.inventory_id, host_id:host.id});
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="modal-content Modal-content">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-title ng-binding">
|
||||
<translate>Disassociate Group</translate>
|
||||
<translate>Disassociate Host From Group</translate>
|
||||
<a href="" id="awp-promote" href=""
|
||||
aw-pop-over="<dl><dt>Disassociate</dt><dd>Disassociates this group from the currently targeted parent group.</dd></dl>"
|
||||
aw-pop-over="<dl><dt>Disassociate</dt><dd>Disassociates this host from the currently targeted parent group.</dd></dl>"
|
||||
aw-tool-tip="Click for help"
|
||||
data-placement="right"
|
||||
data-container="body"
|
||||
data-title="Disassociate Group"
|
||||
data-title="Disassociate Host From Group"
|
||||
class="help-link">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</a>
|
||||
|
||||
@@ -198,7 +198,10 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition',
|
||||
$scope.group_by = $scope.group_by_choices;
|
||||
$scope.groupByPopOver = i18n._("Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.");
|
||||
$scope.instanceFilterPopOver = i18n._("Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.");
|
||||
}
|
||||
}
|
||||
if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') {
|
||||
$scope.instanceFilterPopOver = i18n._("Provide the named URL encoded name or id of the remote Tower inventory to be imported.");
|
||||
}
|
||||
CreateSelect2({
|
||||
element: '#inventory_source_group_by',
|
||||
multiple: true,
|
||||
|
||||
@@ -275,7 +275,7 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString',
|
||||
i18n._("for a complete list of supported filters.") + "</p>";
|
||||
|
||||
}
|
||||
if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') {
|
||||
if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') {
|
||||
add_new = true;
|
||||
$scope.group_by_choices = (inventorySourceData.group_by) ? inventorySourceData.group_by.split(',')
|
||||
.map((i) => ({name: i, label: i, value: i})) : [];
|
||||
@@ -283,6 +283,9 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString',
|
||||
$scope.groupByPopOver = i18n._(`Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`);
|
||||
$scope.instanceFilterPopOver = i18n._(`Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.`);
|
||||
}
|
||||
if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') {
|
||||
$scope.instanceFilterPopOver = i18n._(`Provide the named URL encoded name or id of the remote Tower inventory to be imported.`);
|
||||
}
|
||||
CreateSelect2({
|
||||
element: '#inventory_source_group_by',
|
||||
multiple: true,
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
'ViewUpdateStatus', 'rbacUiControlService', 'GetBasePath',
|
||||
'GetSyncStatusMsg', 'Dataset', 'Find', 'QuerySet',
|
||||
'inventoryData', '$filter', 'Prompt', 'Wait', 'SourcesService', 'inventorySourceOptions',
|
||||
'canAdd', 'hasSyncableSources', 'i18n', 'InventoryHostsStrings', 'InventorySourceModel',
|
||||
'canAdd', 'hasSyncableSources', 'i18n', 'InventoryHostsStrings', 'InventorySourceModel', 'ProcessErrors',
|
||||
function($scope, $rootScope, $state, $stateParams, SourcesListDefinition,
|
||||
InventoryUpdate, CancelSourceUpdate,
|
||||
ViewUpdateStatus, rbacUiControlService, GetBasePath, GetSyncStatusMsg,
|
||||
Dataset, Find, qs, inventoryData, $filter, Prompt,
|
||||
Wait, SourcesService, inventorySourceOptions, canAdd, hasSyncableSources, i18n,
|
||||
InventoryHostsStrings, InventorySource){
|
||||
InventoryHostsStrings, InventorySource, ProcessErrors){
|
||||
|
||||
let inventorySource = new InventorySource();
|
||||
|
||||
@@ -120,24 +120,58 @@
|
||||
$state.go('inventories.edit.inventory_sources.edit', {inventory_source_id: id});
|
||||
};
|
||||
$scope.deleteSource = function(inventory_source){
|
||||
var body = '<div class=\"Prompt-bodyQuery\">' + i18n._('Confirm that you want to permanently delete the inventory source below from the inventory. Deleting this inventory source also deletes its associated groups and hosts.') + '</div><div class=\"Prompt-bodyTarget\">' + $filter('sanitize')(inventory_source.name) + '</div>';
|
||||
var action = function(){
|
||||
delete $rootScope.promptActionBtnClass;
|
||||
$rootScope.promptActionBtnClass = "Modal-errorButton--sourcesDelete";
|
||||
Wait('start');
|
||||
SourcesService.delete(inventory_source.id).then(() => {
|
||||
let hostDelete = SourcesService.deleteHosts(inventory_source.id).catch(({data, status}) => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
let reloadListStateParams = null;
|
||||
|
||||
if($scope.inventory_sources.length === 1 && $state.params.inventory_source_search && !_.isEmpty($state.params.inventory_source_search.page) && $state.params.inventory_source_search.page !== '1') {
|
||||
reloadListStateParams = _.cloneDeep($state.params);
|
||||
reloadListStateParams.inventory_source_search.page = (parseInt(reloadListStateParams.inventory_source_search.page)-1).toString();
|
||||
}
|
||||
if (parseInt($state.params.inventory_source_id) === inventory_source.id) {
|
||||
$state.go('^', reloadListStateParams, {reload: true});
|
||||
} else {
|
||||
$state.go('.', reloadListStateParams, {reload: true});
|
||||
}
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null,
|
||||
{
|
||||
hdr: i18n._('Error!'),
|
||||
msg: i18n._('There was an error deleting inventory source hosts. Returned status: ') +
|
||||
status
|
||||
});
|
||||
});
|
||||
let groupDelete = SourcesService.deleteGroups(inventory_source.id).catch(({data, status}) => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null,
|
||||
{
|
||||
hdr: i18n._('Error!'),
|
||||
msg: i18n._('There was an error deleting inventory source groups. Returned status: ') +
|
||||
status
|
||||
});
|
||||
});
|
||||
Promise.all([hostDelete, groupDelete]).then(() => {
|
||||
SourcesService.delete(inventory_source.id).then(() => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
delete $rootScope.promptActionBtnClass;
|
||||
let reloadListStateParams = null;
|
||||
|
||||
if($scope.inventory_sources.length === 1 && $state.params.inventory_source_search && !_.isEmpty($state.params.inventory_source_search.page) && $state.params.inventory_source_search.page !== '1') {
|
||||
reloadListStateParams = _.cloneDeep($state.params);
|
||||
reloadListStateParams.inventory_source_search.page = (parseInt(reloadListStateParams.inventory_source_search.page)-1).toString();
|
||||
}
|
||||
if (parseInt($state.params.inventory_source_id) === inventory_source.id) {
|
||||
$state.go('^', reloadListStateParams, {reload: true});
|
||||
} else {
|
||||
$state.go('.', reloadListStateParams, {reload: true});
|
||||
}
|
||||
Wait('stop');
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('stop');
|
||||
ProcessErrors($scope, data, status, null,
|
||||
{
|
||||
hdr: i18n._('Error!'),
|
||||
msg: i18n._('There was an error deleting inventory source. Returned status: ') +
|
||||
status
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
inventorySource.getDependentResourceCounts(inventory_source.id)
|
||||
|
||||
@@ -144,7 +144,7 @@ return {
|
||||
instance_filters: {
|
||||
label: i18n._("Instance Filters"),
|
||||
type: 'text',
|
||||
ngShow: "source && (source.value == 'ec2' || source.value == 'vmware')",
|
||||
ngShow: "source && (source.value == 'ec2' || source.value == 'vmware' || source.value == 'tower')",
|
||||
dataTitle: i18n._('Instance Filters'),
|
||||
dataPlacement: 'right',
|
||||
awPopOverWatch: 'instanceFilterPopOver',
|
||||
|
||||
@@ -120,6 +120,34 @@ export default
|
||||
source = source && source.value ? source.value : '';
|
||||
if(source === 'ec2'){
|
||||
return _.map(group_by, 'value').join(',');
|
||||
}
|
||||
if(source === 'vmware'){
|
||||
group_by = _.map(group_by, (i) => {return i.value;});
|
||||
$("#inventory_source_group_by").siblings(".select2").first().find(".select2-selection__choice").each(function(optionIndex, option){
|
||||
group_by.push(option.title);
|
||||
});
|
||||
group_by = (Array.isArray(group_by)) ? _.uniq(group_by).join() : "";
|
||||
return group_by;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
deleteHosts(id) {
|
||||
this.url = GetBasePath('inventory_sources') + id + '/hosts/';
|
||||
Rest.setUrl(this.url);
|
||||
return Rest.destroy()
|
||||
.success(this.success.bind(this))
|
||||
.error(this.error.bind(this))
|
||||
.finally();
|
||||
},
|
||||
deleteGroups(id) {
|
||||
this.url = GetBasePath('inventory_sources') + id + '/groups/';
|
||||
Rest.setUrl(this.url);
|
||||
return Rest.destroy()
|
||||
.success(this.success.bind(this))
|
||||
.error(this.error.bind(this))
|
||||
.finally();
|
||||
}
|
||||
if(source === 'vmware'){
|
||||
group_by = _.map(group_by, (i) => {return i.value;});
|
||||
@@ -134,4 +162,4 @@ export default
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
}];
|
||||
|
||||
@@ -28,6 +28,11 @@ function InventoryHostsStrings (BaseString) {
|
||||
MISSING_PERMISSIONS: t.s('You do not have sufficient permissions to edit the host filter.')
|
||||
}
|
||||
};
|
||||
|
||||
ns.smartinventorybutton = {
|
||||
DISABLED_INSTRUCTIONS: "Please enter at least one search term to create a new Smart Inventory.",
|
||||
ENABLED_INSTRUCTIONS: "Create a new Smart Inventory from search results."
|
||||
};
|
||||
}
|
||||
|
||||
InventoryHostsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
color: @default-interface-txt;
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-break: break-all;
|
||||
width:100%;
|
||||
background-color: @default-secondary-bg;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
}
|
||||
})
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors(scope, data, status, {
|
||||
ProcessErrors(scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to launch adhoc command. POST ' +
|
||||
'returned status: ' + status });
|
||||
|
||||
@@ -27,7 +27,6 @@ export default
|
||||
// scope.$emit('CancelJob');
|
||||
// scope.$destroy();
|
||||
},
|
||||
icon: "fa-times",
|
||||
"class": "btn btn-default",
|
||||
"id": "password-cancel-button"
|
||||
},{
|
||||
@@ -36,7 +35,6 @@ export default
|
||||
scope.$emit(callback);
|
||||
$('#password-modal').dialog('close');
|
||||
},
|
||||
icon: "fa-check",
|
||||
"class": "btn btn-primary",
|
||||
"id": "password-accept-button"
|
||||
}];
|
||||
@@ -46,7 +44,7 @@ export default
|
||||
scope: scope,
|
||||
buttons: buttons,
|
||||
width: 620,
|
||||
height: 700, //(scope.passwords.length > 1) ? 700 : 500,
|
||||
height: "auto",
|
||||
minWidth: 500,
|
||||
title: 'Launch Configuration',
|
||||
callback: 'DialogReady',
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
border-radius: 5px;
|
||||
border: 1px solid @default-link;
|
||||
background-color: @default-link;
|
||||
width: 42px;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
min-width: 42px;
|
||||
|
||||
&.ScheduleToggle--disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -42,10 +40,11 @@
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding-top: 0px;
|
||||
margin-top: 0px;
|
||||
border-top: 0px;
|
||||
border-bottom: 0px;
|
||||
border-right: 0px;
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.ScheduleToggle.is-on {
|
||||
|
||||
@@ -94,7 +94,7 @@ angular.module('ModalDialog', ['Utilities'])
|
||||
ww = $(document).width();
|
||||
wh = $(document).height();
|
||||
x = (width > ww) ? ww - 10 : width;
|
||||
y = (height > wh) ? wh - 10 : height;
|
||||
y = height === "auto" ? "auto" : ((height > wh) ? wh - 10 : height);
|
||||
|
||||
// Create the modal
|
||||
$('#' + id).dialog({
|
||||
|
||||
@@ -636,6 +636,22 @@ angular.module('Utilities', ['RestServices', 'Utilities'])
|
||||
|
||||
if (!multiple) {
|
||||
config.minimumResultsForSearch = 1;
|
||||
config.matcher = function(params, data) {
|
||||
// If there are no search terms, return all of the data
|
||||
if ($.trim(params.term) === '' &&
|
||||
data.text.indexOf("Choose an") === -1) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Do not display the item if there is no 'text' property
|
||||
if (typeof data.text === 'undefined' ||
|
||||
data.text.indexOf("Choose an") > -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return `null` if the term should not be displayed
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,35 @@ export default ['templateUrl', function(templateUrl) {
|
||||
|
||||
element.find('.modal-body').append(clone);
|
||||
|
||||
scope.init();
|
||||
scope.init(element);
|
||||
|
||||
});
|
||||
$('#form-modal').modal('show');
|
||||
},
|
||||
controller: ['$scope', '$state', function($scope, $state) {
|
||||
controller: ['$scope', '$state', 'EventService', function($scope, $state, eventService) {
|
||||
let listeners, modal;
|
||||
|
||||
$scope.init = function() {
|
||||
function clickIsOutsideModal(e) {
|
||||
const m = modal.getBoundingClientRect();
|
||||
const cx = e.clientX;
|
||||
const cy = e.clientY;
|
||||
|
||||
if (cx < m.left || cx > m.right || cy > m.bottom || cy < m.top) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function clickToHide(event) {
|
||||
if (clickIsOutsideModal(event)) {
|
||||
$scope.cancelForm();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.init = function(el) {
|
||||
let list = $scope.list;
|
||||
[modal] = el.find('.modal-dialog');
|
||||
if($state.params.selected) {
|
||||
let selection = $scope[list.name].find(({id}) => id === parseInt($state.params.selected));
|
||||
$scope.currentSelection = _.pick(selection, 'id', 'name');
|
||||
@@ -37,6 +57,10 @@ export default ['templateUrl', function(templateUrl) {
|
||||
});
|
||||
|
||||
$scope.modalTitle = list.iterator.replace(/_/g, ' ');
|
||||
|
||||
listeners = eventService.addListeners([
|
||||
[window, 'click', clickToHide]
|
||||
]);
|
||||
};
|
||||
|
||||
function selectRowIfPresent(){
|
||||
@@ -51,6 +75,7 @@ export default ['templateUrl', function(templateUrl) {
|
||||
}
|
||||
|
||||
$scope.saveForm = function() {
|
||||
eventService.remove(listeners);
|
||||
let list = $scope.list;
|
||||
if($scope.currentSelection.name !== null) {
|
||||
$scope.$parent[`${list.iterator}_name`] = $scope.currentSelection.name;
|
||||
@@ -60,6 +85,7 @@ export default ['templateUrl', function(templateUrl) {
|
||||
};
|
||||
|
||||
$scope.cancelForm = function() {
|
||||
eventService.remove(listeners);
|
||||
$state.go('^');
|
||||
};
|
||||
|
||||
|
||||
@@ -97,6 +97,10 @@
|
||||
color: @btn-txt-sel;
|
||||
}
|
||||
|
||||
.Modal-errorButton--sourcesDelete:hover{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Modal-primaryButton {
|
||||
background-color: @default-link;
|
||||
color: @default-bg;
|
||||
|
||||
@@ -29,43 +29,48 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
replaceDefaultFlags (value) {
|
||||
value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
value = value.toString().replace(/__search_DEFAULT/g, "__search");
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
replaceEncodedTokens(value) {
|
||||
return decodeURIComponent(value).replace(/"|'/g, "");
|
||||
},
|
||||
|
||||
encodeTerms (values, key) {
|
||||
key = this.replaceDefaultFlags(key);
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = this.replaceEncodedTokens(values);
|
||||
|
||||
return `${key}=${values}`;
|
||||
}
|
||||
|
||||
return values
|
||||
.map(value => {
|
||||
value = this.replaceDefaultFlags(value);
|
||||
value = this.replaceEncodedTokens(value);
|
||||
|
||||
return `${key}=${value}`;
|
||||
})
|
||||
.join('&');
|
||||
},
|
||||
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
|
||||
encodeQueryset(params) {
|
||||
let queryset;
|
||||
queryset = _.reduce(params, (result, value, key) => {
|
||||
return result + encodeTerm(value, key);
|
||||
}, '');
|
||||
queryset = queryset.substring(0, queryset.length - 1);
|
||||
return angular.isObject(params) ? `?${queryset}` : '';
|
||||
|
||||
function encodeTerm(value, key){
|
||||
|
||||
key = key.toString().replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
key = key.toString().replace(/__search_DEFAULT/g, "__search");
|
||||
|
||||
value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains");
|
||||
value = value.toString().replace(/__search_DEFAULT/g, "__search");
|
||||
|
||||
if (Array.isArray(value)){
|
||||
value = _.uniq(_.flattenDeep(value));
|
||||
let concated = '';
|
||||
angular.forEach(value, function(item){
|
||||
if(item && typeof item === 'string') {
|
||||
item = decodeURIComponent(item).replace(/"|'/g, "");
|
||||
}
|
||||
concated += `${key}=${item}&`;
|
||||
});
|
||||
|
||||
return concated;
|
||||
}
|
||||
else {
|
||||
if(value && typeof value === 'string') {
|
||||
value = decodeURIComponent(value).replace(/"|'/g, "");
|
||||
}
|
||||
|
||||
return `${key}=${value}&`;
|
||||
}
|
||||
if (typeof params !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return _.reduce(params, (result, value, key) => {
|
||||
if (result !== '?') {
|
||||
result += '&';
|
||||
}
|
||||
|
||||
return result += this.encodeTerms(value, key);
|
||||
}, '?');
|
||||
},
|
||||
// encodes a ui smart-search param to a django-friendly param
|
||||
// operand:key:comparator:value => {operand__key__comparator: value}
|
||||
@@ -127,7 +132,7 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
|
||||
}
|
||||
|
||||
if(params.singleSearchParam) {
|
||||
return {[params.singleSearchParam]: encodeURIComponent(paramString + "=" + valueString)};
|
||||
return {[params.singleSearchParam]: paramString + "=" + valueString};
|
||||
}
|
||||
else {
|
||||
return {[paramString] : encodeURIComponent(valueString)};
|
||||
|
||||
@@ -140,7 +140,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
|
||||
function searchWithoutKey(term) {
|
||||
if($scope.singleSearchParam) {
|
||||
return {
|
||||
[$scope.singleSearchParam]: encodeURIComponent("search=" + term)
|
||||
[$scope.singleSearchParam]: "search=" + encodeURIComponent(term)
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -328,6 +328,10 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
|
||||
else {
|
||||
if($scope.singleSearchParam && set[$scope.singleSearchParam] && set[$scope.singleSearchParam].includes("%20and%20")) {
|
||||
let searchParamParts = set[$scope.singleSearchParam].split("%20and%20");
|
||||
// The value side of each paramPart might have been encoded in SmartSearch.splitFilterIntoTerms
|
||||
_.each(searchParamParts, (paramPart, paramPartIndex) => {
|
||||
searchParamParts[paramPartIndex] = decodeURIComponent(paramPart);
|
||||
});
|
||||
var index = searchParamParts.indexOf(value);
|
||||
if (index !== -1) {
|
||||
searchParamParts.splice(index, 1);
|
||||
|
||||
@@ -341,7 +341,6 @@
|
||||
Rest.post(data)
|
||||
.then(({data}) => {
|
||||
|
||||
Wait('stop');
|
||||
if (data.related && data.related.callback) {
|
||||
Alert('Callback URL',
|
||||
`Host callbacks are enabled for this template. The callback URL is:
|
||||
|
||||
@@ -501,7 +501,7 @@ export default
|
||||
$scope.removeTemplateSaveSuccess();
|
||||
}
|
||||
$scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) {
|
||||
Wait('stop');
|
||||
|
||||
if (data.related &&
|
||||
data.related.callback) {
|
||||
Alert('Callback URL',
|
||||
@@ -610,6 +610,7 @@ export default
|
||||
}
|
||||
$q.all(defers)
|
||||
.then(function() {
|
||||
Wait('stop');
|
||||
saveCompleted();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,13 +157,13 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
|
||||
view_survey: {
|
||||
ngClick: 'editSurvey()',
|
||||
awFeature: 'surveys',
|
||||
ngShow: '($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\')) && survey_exists && !(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)',
|
||||
ngShow: '($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\')) && survey_exists && !(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)',
|
||||
label: i18n._('View Survey'),
|
||||
class: 'Form-primaryButton'
|
||||
},
|
||||
add_survey: {
|
||||
ngClick: 'addSurvey()',
|
||||
ngShow: '!survey_exists && ($state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\'))',
|
||||
ngShow: '!survey_exists && ($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\'))',
|
||||
awFeature: 'surveys',
|
||||
awToolTip: '{{surveyTooltip}}',
|
||||
dataPlacement: 'top',
|
||||
@@ -173,7 +173,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
|
||||
edit_survey: {
|
||||
ngClick: 'editSurvey()',
|
||||
awFeature: 'surveys',
|
||||
ngShow: 'survey_exists && ($state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\'))',
|
||||
ngShow: 'survey_exists && ($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\'))',
|
||||
label: i18n._('Edit Survey'),
|
||||
class: 'Form-primaryButton',
|
||||
awToolTip: '{{surveyTooltip}}',
|
||||
@@ -181,7 +181,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
|
||||
},
|
||||
workflow_editor: {
|
||||
ngClick: 'openWorkflowMaker()',
|
||||
ngShow: '$state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\')',
|
||||
ngShow: '$state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\')',
|
||||
awToolTip: '{{workflowEditorTooltip}}',
|
||||
dataPlacement: 'top',
|
||||
label: i18n._('Workflow Editor'),
|
||||
|
||||
@@ -21,7 +21,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
restrict: 'E',
|
||||
link: function(scope, element) {
|
||||
|
||||
let margin = {top: 20, right: 20, bottom: 20, left: 20},
|
||||
let marginLeft = 20,
|
||||
i = 0,
|
||||
nodeW = 180,
|
||||
nodeH = 60,
|
||||
@@ -36,7 +36,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
line,
|
||||
zoomObj,
|
||||
baseSvg,
|
||||
svgGroup;
|
||||
svgGroup,
|
||||
graphLoaded;
|
||||
|
||||
scope.dimensionsSet = false;
|
||||
|
||||
@@ -75,7 +76,8 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
);
|
||||
|
||||
svgGroup = baseSvg.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
|
||||
.attr("id", "aw-workflow-chart-g")
|
||||
.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
|
||||
}
|
||||
|
||||
function calcAvailableScreenSpace() {
|
||||
@@ -158,7 +160,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
let scale = d3.event.scale,
|
||||
translation = d3.event.translate;
|
||||
|
||||
translation = [translation[0] + (margin.left*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
|
||||
translation = [translation[0] + (marginLeft*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
|
||||
|
||||
svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
|
||||
|
||||
@@ -177,7 +179,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
|
||||
translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
|
||||
|
||||
svgGroup.attr("transform", "translate(" + [translateX + (margin.left*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
|
||||
svgGroup.attr("transform", "translate(" + [translateX + (marginLeft*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
|
||||
zoomObj.scale(scale);
|
||||
zoomObj.translate([translateX, translateY]);
|
||||
}
|
||||
@@ -200,12 +202,36 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
}
|
||||
|
||||
function resetZoomAndPan() {
|
||||
svgGroup.attr("transform", "translate(" + margin.left + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
|
||||
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
|
||||
// Update the zoomObj
|
||||
zoomObj.scale(1);
|
||||
zoomObj.translate([0,0]);
|
||||
}
|
||||
|
||||
function zoomToFitChart() {
|
||||
let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
|
||||
startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
|
||||
availableScreenSpace = calcAvailableScreenSpace(),
|
||||
currentZoomValue = zoomObj.scale(),
|
||||
unscaledH = graphDimensions.height/currentZoomValue,
|
||||
unscaledW = graphDimensions.width/currentZoomValue,
|
||||
scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
|
||||
scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft)/unscaledW,
|
||||
lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
|
||||
scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10),
|
||||
startNodeOffsetFromGraphCenter = Math.round((((rootH/2) + (startNodeDimensions.top/currentZoomValue)) - ((graphDimensions.top/currentZoomValue) + (unscaledH/2)))*scaleToFit);
|
||||
|
||||
manualZoom(scaleToFit*100);
|
||||
|
||||
scope.workflowZoomed({
|
||||
zoom: scaleToFit
|
||||
});
|
||||
|
||||
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter) + ")scale(" + scaleToFit + ")");
|
||||
zoomObj.translate([marginLeft - scaleToFit*marginLeft, windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
|
||||
|
||||
}
|
||||
|
||||
function update() {
|
||||
let userCanAddEdit = (scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate;
|
||||
if(scope.dimensionsSet) {
|
||||
@@ -243,7 +269,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
thisNode.append("rect")
|
||||
.attr("width", rootW)
|
||||
.attr("height", rootH)
|
||||
//.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH)
|
||||
.attr("y", 10)
|
||||
.attr("rx", 5)
|
||||
.attr("ry", 5)
|
||||
@@ -252,7 +277,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
.call(add_node);
|
||||
thisNode.append("text")
|
||||
.attr("x", 13)
|
||||
//.attr("y", (windowHeight-margin.top-margin.bottom)/2 - rootH + rootH/2)
|
||||
.attr("y", 30)
|
||||
.attr("dy", ".35em")
|
||||
.attr("class", "WorkflowChart-startText")
|
||||
@@ -544,6 +568,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
|
||||
node.exit().remove();
|
||||
|
||||
if(nodes && nodes.length > 1 && !graphLoaded) {
|
||||
zoomToFitChart();
|
||||
}
|
||||
|
||||
graphLoaded = true;
|
||||
|
||||
let link = svgGroup.selectAll("g.link")
|
||||
.data(links, function(d) {
|
||||
return d.source.id + "-" + d.target.id;
|
||||
@@ -927,6 +957,10 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
|
||||
manualZoom(params.zoom);
|
||||
});
|
||||
|
||||
scope.$on('zoomToFitChart', function() {
|
||||
zoomToFitChart();
|
||||
});
|
||||
|
||||
let clearWatchTreeData = scope.$watch('treeData', function(newVal) {
|
||||
if(newVal) {
|
||||
update();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.WorkflowControls-Pan {
|
||||
flex: 0 0 85px;
|
||||
flex: 0 0 87px;
|
||||
}
|
||||
.WorkflowControls-Pan--button {
|
||||
color: @default-icon;
|
||||
@@ -49,17 +49,10 @@
|
||||
.WorkflowControls-Zoom--button:hover {
|
||||
color: @default-link-hov;
|
||||
}
|
||||
.WorkflowControls-Zoom--minus {
|
||||
margin-left: 20px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.WorkflowControls-Zoom--plus {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.WorkflowControls-zoomSlider {
|
||||
width: 150px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.WorkflowControls-zoomPercentage {
|
||||
text-align: center;
|
||||
@@ -67,6 +60,16 @@
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.WorkflowControls-ZoomToFit {
|
||||
display: flex;
|
||||
flex: 0 0 62px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: @default-icon;
|
||||
}
|
||||
.WorkflowControls-ZoomToFit:hover {
|
||||
color: @default-link-hov;
|
||||
}
|
||||
|
||||
.ui-slider-handle.ui-state-default.ui-corner-all {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -10,7 +10,8 @@ export default ['templateUrl',
|
||||
scope: {
|
||||
panChart: '&',
|
||||
resetChart: '&',
|
||||
zoomChart: '&'
|
||||
zoomChart: '&',
|
||||
zoomToFitChart: '&'
|
||||
},
|
||||
templateUrl: templateUrl('templates/workflows/workflow-controls/workflow-controls'),
|
||||
restrict: 'E',
|
||||
@@ -60,6 +61,10 @@ export default ['templateUrl',
|
||||
});
|
||||
};
|
||||
|
||||
scope.zoomToFit = function() {
|
||||
scope.zoomToFitChart();
|
||||
};
|
||||
|
||||
scope.$on('workflowZoomed', function(evt, params) {
|
||||
scope.zoom = Math.round(params.zoom * 10) * 10;
|
||||
$("#slider").slider('value',scope.zoom);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<div class="WorkflowControls-ZoomToFit">
|
||||
<i class="fa fa-desktop" ng-click="zoomToFit()"></i>
|
||||
</div>
|
||||
<div class="WorkflowControls-Zoom">
|
||||
<div class="WorkflowControls-Zoom--button WorkflowControls-Zoom--minus" ng-click="zoomOut()">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.WorkflowLegend-maker--right {
|
||||
flex: 0 0 215px;
|
||||
flex: 0 0 217px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
position: relative;
|
||||
@@ -246,9 +246,9 @@
|
||||
}
|
||||
.WorkflowMaker-manualControls {
|
||||
position: absolute;
|
||||
left: -77px;
|
||||
left: -106px;
|
||||
height: 60px;
|
||||
width: 293px;
|
||||
width: 322px;
|
||||
background-color: @default-bg;
|
||||
display: flex;
|
||||
border: 1px solid @b7grey;
|
||||
@@ -259,10 +259,10 @@
|
||||
}
|
||||
.WorkflowLegend-manualControls {
|
||||
position: absolute;
|
||||
left: -239px;
|
||||
left: -272px;
|
||||
top: 38px;
|
||||
height: 60px;
|
||||
width: 290px;
|
||||
width: 322px;
|
||||
background-color: @default-bg;
|
||||
display: flex;
|
||||
border: 1px solid @d7grey;
|
||||
|
||||
@@ -31,9 +31,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
showTypeOptions: false
|
||||
};
|
||||
|
||||
$scope.editRequests = [];
|
||||
$scope.associateRequests = [];
|
||||
$scope.disassociateRequests = [];
|
||||
$scope.editRequests = [];
|
||||
$scope.associateRequests = [];
|
||||
$scope.disassociateRequests = [];
|
||||
|
||||
$scope.showKey = false;
|
||||
$scope.toggleKey = () => $scope.showKey = !$scope.showKey;
|
||||
@@ -940,9 +940,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('WorkflowDialogReady', function(){
|
||||
$scope.modalOpen = true;
|
||||
});
|
||||
$scope.zoomToFitChart = function() {
|
||||
$scope.$broadcast('zoomToFitChart');
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window',
|
||||
scope.removeWorkflowDialogReady = scope.$on('WorkflowDialogReady', function() {
|
||||
$('#workflow-modal-dialog').dialog('open');
|
||||
|
||||
scope.modalOpen = true;
|
||||
|
||||
scope.$broadcast("refreshWorkflowChart");
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<span class="badge List-titleBadge" ng-bind="treeData.data.totalNodes"></span>
|
||||
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
|
||||
<div ng-show="showManualControls" class="WorkflowMaker-manualControls noselect">
|
||||
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()"></workflow-controls>
|
||||
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()" zoom-to-fit-chart="zoomToFitChart()"></workflow-controls>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,6 +165,10 @@ export default ['workflowData', 'workflowResultsService', 'workflowDataOptions',
|
||||
$scope.$broadcast('resetWorkflowChart');
|
||||
};
|
||||
|
||||
$scope.zoomToFitChart = function() {
|
||||
$scope.$broadcast('zoomToFitChart');
|
||||
};
|
||||
|
||||
$scope.workflowZoomed = function(zoom) {
|
||||
$scope.$broadcast('workflowZoomed', {
|
||||
zoom: zoom
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
<div class="WorkflowLegend-details--right">
|
||||
<i ng-class="{'WorkflowMaker-manualControlsIcon--active': showManualControls}" class="fa fa-cog WorkflowMaker-manualControlsIcon" aria-hidden="true" alt="Controls" ng-click="toggleManualControls()"></i>
|
||||
<div ng-show="showManualControls" class="WorkflowLegend-manualControls noselect">
|
||||
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()"></workflow-controls>
|
||||
<workflow-controls class="WorkflowControls" pan-chart="panChart(direction)" zoom-chart="zoomChart(zoom)" reset-chart="resetChart()" zoom-to-fit-chart="zoomToFitChart()"></workflow-controls>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,3 +52,14 @@ register(
|
||||
category_slug='ui',
|
||||
feature_required='rebranding',
|
||||
)
|
||||
|
||||
register(
|
||||
'MAX_UI_JOB_EVENTS',
|
||||
field_class=fields.IntegerField,
|
||||
min_value=100,
|
||||
label=_('Max Job Events Retreived by UI'),
|
||||
help_text=_('Maximum number of job events for the UI to retreive within a '
|
||||
'single request.'),
|
||||
category=_('UI'),
|
||||
category_slug='ui',
|
||||
)
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
"dev": "webpack --config build/webpack.development.js --progress",
|
||||
"watch": "webpack-dev-server --config build/webpack.watch.js --progress",
|
||||
"production": "webpack --config build/webpack.production.js"
|
||||
"component-test": "./node_modules/.bin/karma start client/test/karma.conf.js",
|
||||
"lint-dev": "./node_modules/.bin/nodemon --exec \"./node_modules/.bin/eslint -c .eslintrc.js .\" --watch \"client/components/**/*.js\"",
|
||||
"component-dev": "./node_modules/.bin/karma start client/test/karma.conf.js --auto-watch --no-single-run"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "*",
|
||||
"browser-sync": "^2.14.0",
|
||||
"grunt-browser-sync": "^2.2.0",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"webpack-dev-server": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.6.6",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2036
awx/ui/po/es.po
2036
awx/ui/po/es.po
File diff suppressed because it is too large
Load Diff
2042
awx/ui/po/fr.po
2042
awx/ui/po/fr.po
File diff suppressed because it is too large
Load Diff
2009
awx/ui/po/ja.po
2009
awx/ui/po/ja.po
File diff suppressed because it is too large
Load Diff
2035
awx/ui/po/nl.po
2035
awx/ui/po/nl.po
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user