mirror of
https://github.com/ansible/awx.git
synced 2024-11-02 18:21:12 +03:00
AC-363 token expiration now handled exclusively on server.
This commit is contained in:
parent
1763d373eb
commit
f8e1d2e0e3
@ -230,8 +230,8 @@ angular.module('ansible', [
|
|||||||
|
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
}])
|
}])
|
||||||
.run(['$rootScope', 'CheckLicense', '$location', 'Authorization','LoadBasePaths', 'ViewLicense',
|
.run(['$cookieStore', '$rootScope', 'CheckLicense', '$location', 'Authorization','LoadBasePaths', 'ViewLicense',
|
||||||
function($rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense) {
|
function($cookieStore, $rootScope, CheckLicense, $location, Authorization, LoadBasePaths, ViewLicense) {
|
||||||
|
|
||||||
LoadBasePaths();
|
LoadBasePaths();
|
||||||
|
|
||||||
@ -239,8 +239,8 @@ angular.module('ansible', [
|
|||||||
$rootScope.crumbCache = new Array();
|
$rootScope.crumbCache = new Array();
|
||||||
|
|
||||||
$rootScope.$on("$routeChangeStart", function(event, next, current) {
|
$rootScope.$on("$routeChangeStart", function(event, next, current) {
|
||||||
// Evaluate the token on each navigation request. Redirect to login page when not valid
|
// On each navigation request, check that the user is logged in
|
||||||
if (Authorization.isTokenValid() == false) {
|
if (Authorization.isUserLoggedIn() == false) {
|
||||||
if ( next.templateUrl != (urlPrefix + 'partials/login.html') ) {
|
if ( next.templateUrl != (urlPrefix + 'partials/login.html') ) {
|
||||||
$location.path('/login');
|
$location.path('/login');
|
||||||
}
|
}
|
||||||
@ -262,8 +262,10 @@ angular.module('ansible', [
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (! Authorization.isTokenValid() ) {
|
if (!Authorization.getToken()) {
|
||||||
// When the app first loads, redirect to login page
|
// When the app first loads, redirect to login page
|
||||||
|
$rootScope.sessionExpired = false;
|
||||||
|
$cookieStore.put('sessionExpired', false);
|
||||||
$location.path('/login');
|
$location.path('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var $AnsibleConfig =
|
var $AnsibleConfig =
|
||||||
{
|
{
|
||||||
session_timeout: 3600, // cookie expiration in seconds. session will expire after this many
|
|
||||||
// seconds of inactivity.
|
|
||||||
|
|
||||||
tooltip_delay: {show: 500, hide: 100}, // Default number of milliseconds to delay displaying/hiding tooltips
|
tooltip_delay: {show: 500, hide: 100}, // Default number of milliseconds to delay displaying/hiding tooltips
|
||||||
|
|
||||||
debug_mode: true, // Enable console logging messages
|
debug_mode: true, // Enable console logging messages
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function Authenticate($window, $scope, $rootScope, $location, Authorization, ToggleClass, Alert)
|
function Authenticate($cookieStore, $window, $scope, $rootScope, $location, Authorization, ToggleClass, Alert)
|
||||||
{
|
{
|
||||||
var setLoginFocus = function() {
|
var setLoginFocus = function() {
|
||||||
$('#login-username').focus();
|
$('#login-username').focus();
|
||||||
@ -18,6 +18,7 @@ function Authenticate($window, $scope, $rootScope, $location, Authorization, Tog
|
|||||||
|
|
||||||
// Display the login dialog
|
// Display the login dialog
|
||||||
$('#login-modal').modal({ show: true, keyboard: false, backdrop: 'static' });
|
$('#login-modal').modal({ show: true, keyboard: false, backdrop: 'static' });
|
||||||
|
|
||||||
// Set focus to username field
|
// Set focus to username field
|
||||||
$('#login-modal').on('shown.bs.modal', function() {
|
$('#login-modal').on('shown.bs.modal', function() {
|
||||||
setLoginFocus();
|
setLoginFocus();
|
||||||
@ -36,18 +37,8 @@ function Authenticate($window, $scope, $rootScope, $location, Authorization, Tog
|
|||||||
Authorization.logout();
|
Authorization.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.sessionTimeout = ($AnsibleConfig.session_timeout / 60).toFixed(2);
|
$rootScope.userLoggedIn = false; //hide the logout link. if you got here, you're logged out.
|
||||||
|
$cookieStore.put('userLoggedIn', false); //gets set back to true by Authorization.setToken().
|
||||||
if ($rootScope.userLoggedIn) {
|
|
||||||
// If we're logged in, check for session timeout
|
|
||||||
scope.sessionExpired = Authorization.didSessionExpire();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
scope.sessionExpired = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.userLoggedIn = false; //hide the logout link. if you got here, you're logged out.
|
|
||||||
//gets set back to true by Authorization.setToken().
|
|
||||||
|
|
||||||
$('#login-password').bind('keypress', function(e) {
|
$('#login-password').bind('keypress', function(e) {
|
||||||
var code = (e.keyCode ? e.keyCode : e.which);
|
var code = (e.keyCode ? e.keyCode : e.which);
|
||||||
@ -73,16 +64,7 @@ function Authenticate($window, $scope, $rootScope, $location, Authorization, Tog
|
|||||||
.success( function(data, status, headers, config) {
|
.success( function(data, status, headers, config) {
|
||||||
$('#login-modal').modal('hide');
|
$('#login-modal').modal('hide');
|
||||||
token = data.token;
|
token = data.token;
|
||||||
Authorization.setToken(data.token);
|
Authorization.setToken(data.token, data.expires);
|
||||||
scope.reset();
|
|
||||||
|
|
||||||
// Force request to /organizations to query with the correct token -in the event a new user
|
|
||||||
// has logged in.
|
|
||||||
var today = new Date();
|
|
||||||
today.setTime(today.getTime() + ($AnsibleConfig.session_timeout * 1000));
|
|
||||||
$rootScope.token = token;
|
|
||||||
$rootScope.userLoggedIn = true;
|
|
||||||
$rootScope.token_expire = today.getTime();
|
|
||||||
|
|
||||||
// Get all the profile/access info regarding the logged in user
|
// Get all the profile/access info regarding the logged in user
|
||||||
Authorization.getUser()
|
Authorization.getUser()
|
||||||
@ -126,5 +108,5 @@ function Authenticate($window, $scope, $rootScope, $location, Authorization, Tog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Authenticate.$inject = ['$window', '$scope', '$rootScope', '$location', 'Authorization', 'ToggleClass', 'Alert'];
|
Authenticate.$inject = ['$cookieStore', '$window', '$scope', '$rootScope', '$location', 'Authorization', 'ToggleClass', 'Alert'];
|
||||||
|
|
||||||
|
@ -29,6 +29,9 @@ function CredentialsList ($scope, $rootScope, $location, $log, $routeParams, Res
|
|||||||
|
|
||||||
SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 });
|
SelectionInit({ scope: scope, list: list, url: url, returnToCaller: 1 });
|
||||||
|
|
||||||
|
if (scope.PostRefreshRemove) {
|
||||||
|
scope.PostRefreshRemove();
|
||||||
|
}
|
||||||
scope.PostRefershRemove = scope.$on('PostRefresh', function() {
|
scope.PostRefershRemove = scope.$on('PostRefresh', function() {
|
||||||
// After a refresh, populate the organization name on each row
|
// After a refresh, populate the organization name on each row
|
||||||
for(var i=0; i < scope.credentials.length; i++) {
|
for(var i=0; i < scope.credentials.length; i++) {
|
||||||
|
@ -176,7 +176,7 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout
|
|||||||
})
|
})
|
||||||
.error( function(data, status, headers, config) {
|
.error( function(data, status, headers, config) {
|
||||||
ProcessErrors(scope, data, status, form,
|
ProcessErrors(scope, data, status, form,
|
||||||
{ hdr: 'Error!', msg: 'Failed to retrieve organization: ' + $routeParams.id + '. GET status: ' + status });
|
{ hdr: 'Error!', msg: 'Failed to retrieve organization: ' + $routeParams.id + '. GET status: ' + status });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ function OrganizationsEdit ($scope, $rootScope, $compile, $location, $log, $rout
|
|||||||
})
|
})
|
||||||
.error( function(data, status, headers, config) {
|
.error( function(data, status, headers, config) {
|
||||||
ProcessErrors(scope, data, status, OrganizationForm,
|
ProcessErrors(scope, data, status, OrganizationForm,
|
||||||
{ hdr: 'Error!', msg: 'Failed to update organization: ' + id + '. PUT status: ' + status });
|
{ hdr: 'Error!', msg: 'Failed to update organization: ' + id + '. PUT status: ' + status });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ angular.module('JobTemplateFormDefinition', [])
|
|||||||
addRequired: true,
|
addRequired: true,
|
||||||
editRequired: true,
|
editRequired: true,
|
||||||
ngClick: 'lookUpInventory()',
|
ngClick: 'lookUpInventory()',
|
||||||
|
awRequiredWhen: {variable: "inventoryrequired", init: "true" },
|
||||||
column: 1
|
column: 1
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
@ -64,6 +65,7 @@ angular.module('JobTemplateFormDefinition', [])
|
|||||||
addRequired: true,
|
addRequired: true,
|
||||||
editRequired: true,
|
editRequired: true,
|
||||||
ngClick: 'lookUpProject()',
|
ngClick: 'lookUpProject()',
|
||||||
|
awRequiredWhen: {variable: "projectrequired", init: "true" },
|
||||||
column: 1
|
column: 1
|
||||||
},
|
},
|
||||||
playbook: {
|
playbook: {
|
||||||
@ -73,6 +75,7 @@ angular.module('JobTemplateFormDefinition', [])
|
|||||||
id: 'playbook-select',
|
id: 'playbook-select',
|
||||||
addRequired: true,
|
addRequired: true,
|
||||||
editRequired: true,
|
editRequired: true,
|
||||||
|
awRequiredWhen: {variable: "playbookrequired", init: "true" },
|
||||||
column: 1
|
column: 1
|
||||||
},
|
},
|
||||||
credential: {
|
credential: {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities'])
|
angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities'])
|
||||||
.factory('RefreshRelated', ['Alert', 'Rest', function(Alert, Rest) {
|
.factory('RefreshRelated', ['ProcessErrors', 'Rest', function(ProcessErrors, Rest) {
|
||||||
return function(params) {
|
return function(params) {
|
||||||
|
|
||||||
var scope = params.scope;
|
var scope = params.scope;
|
||||||
@ -40,7 +40,8 @@ angular.module('RefreshRelatedHelper', ['RestServices', 'Utilities'])
|
|||||||
})
|
})
|
||||||
.error ( function(data, status, headers, config) {
|
.error ( function(data, status, headers, config) {
|
||||||
scope[iterator + 'SearchSpin'] = true;
|
scope[iterator + 'SearchSpin'] = true;
|
||||||
Alert('Error!', 'Failed to retrieve related set: ' + set + '. GET returned status: ' + status);
|
ProcessErrors(scope, data, status, null,
|
||||||
|
{ hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RefreshHelper', ['RestServices', 'Utilities'])
|
angular.module('RefreshHelper', ['RestServices', 'Utilities'])
|
||||||
.factory('Refresh', ['Alert', 'Rest', function(Alert, Rest) {
|
.factory('Refresh', ['ProcessErrors', 'Rest', function(ProcessErrors, Rest) {
|
||||||
return function(params) {
|
return function(params) {
|
||||||
|
|
||||||
var scope = params.scope;
|
var scope = params.scope;
|
||||||
@ -37,7 +37,8 @@ angular.module('RefreshHelper', ['RestServices', 'Utilities'])
|
|||||||
})
|
})
|
||||||
.error ( function(data, status, headers, config) {
|
.error ( function(data, status, headers, config) {
|
||||||
scope[iterator + 'SearchSpin'] = false;
|
scope[iterator + 'SearchSpin'] = false;
|
||||||
Alert('Error!', 'Failed to retrieve ' + set + '. GET returned status: ' + status);
|
ProcessErrors(scope, data, status, null,
|
||||||
|
{ hdr: 'Error!', msg: 'Failed to retrieve ' + set + '. GET returned status: ' + status });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
@ -13,11 +13,11 @@ angular.module('JobHostDefinition', [])
|
|||||||
name: 'jobhosts',
|
name: 'jobhosts',
|
||||||
iterator: 'jobhost',
|
iterator: 'jobhost',
|
||||||
editTitle: 'Job Host Summary',
|
editTitle: 'Job Host Summary',
|
||||||
index: true,
|
indexShow: 'host_id == null',
|
||||||
hover: true,
|
hover: true,
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
job: {
|
id: {
|
||||||
label: 'Job ID',
|
label: 'Job ID',
|
||||||
ngClick: "showJob(\{\{ jobhost.job \}\})",
|
ngClick: "showJob(\{\{ jobhost.job \}\})",
|
||||||
columnShow: 'host_id !== null',
|
columnShow: 'host_id !== null',
|
||||||
|
@ -1,67 +1,45 @@
|
|||||||
/*********************************************
|
/*********************************************
|
||||||
* Copyright (c) 2013 AnsibleWorks, Inc.
|
* Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
*
|
*
|
||||||
* User authentication functions
|
* AuthService.js
|
||||||
*
|
*
|
||||||
|
* User authentication functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('AuthService', ['ngCookies'])
|
angular.module('AuthService', ['ngCookies', 'Utilities'])
|
||||||
.factory('Authorization', ['$http', '$rootScope', '$location', '$cookieStore', function($http, $rootScope, $location, $cookieStore) {
|
.factory('Authorization', ['$http', '$rootScope', '$location', '$cookieStore', 'GetBasePath',
|
||||||
|
function($http, $rootScope, $location, $cookieStore, GetBasePath) {
|
||||||
return {
|
return {
|
||||||
setToken: function(token) {
|
setToken: function(token, expires) {
|
||||||
// set the session cookie
|
// set the session cookie
|
||||||
var today = new Date();
|
|
||||||
today.setTime(today.getTime() + ($AnsibleConfig.session_timeout * 1000));
|
|
||||||
$cookieStore.remove('token');
|
$cookieStore.remove('token');
|
||||||
$cookieStore.remove('token_expire');
|
$cookieStore.remove('token_expires');
|
||||||
|
$cookieStore.remove('userLoggedIn');
|
||||||
$cookieStore.put('token', token);
|
$cookieStore.put('token', token);
|
||||||
$cookieStore.put('token_expire', today.getTime());
|
$cookieStore.put('token_expires', expires);
|
||||||
|
$cookieStore.put('userLoggedIn', true);
|
||||||
|
$cookieStore.put('sessionExpired', false);
|
||||||
$rootScope.token = token;
|
$rootScope.token = token;
|
||||||
$rootScope.userLoggedIn = true;
|
$rootScope.userLoggedIn = true;
|
||||||
$rootScope.token_expire = today.getTime();
|
$rootScope.token_expires = expires;
|
||||||
|
$rootScope.sessionExpired = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isTokenValid: function() {
|
isUserLoggedIn: function() {
|
||||||
// check if token exists and is not expired
|
if ($rootScope.userLoggedIn == undefined) {
|
||||||
var response = false;
|
// Browser refresh may have occurred
|
||||||
var token = ($rootScope.token) ? $rootScope.token : $cookieStore.get('token');
|
$rootScope.userLoggedIn = $cookieStore.get('userLoggedIn');
|
||||||
var token_expire = ($rootScope.token_expire) ? $rootScope.token_expire : $cookieStore.get('token_expire');
|
$rootScope.sessionExpired = $cookieStore.get('sessionExpired');
|
||||||
if (token && token_expire) {
|
|
||||||
var exp = new Date(token_expire);
|
|
||||||
var today = new Date();
|
|
||||||
if (today < exp) {
|
|
||||||
this.setToken(token); //push expiration into the future while user is active
|
|
||||||
response = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return response;
|
return $rootScope.userLoggedIn;
|
||||||
},
|
},
|
||||||
|
|
||||||
didSessionExpire: function() {
|
|
||||||
// use only to test why user was sent to login page.
|
|
||||||
var response = false;
|
|
||||||
var token_expire = ($rootScope.token_expire) ? $rootScope.token_expire : $cookieStore.get('token_expire');
|
|
||||||
if (token_expire) {
|
|
||||||
var exp = new Date(token_expire);
|
|
||||||
var today = new Date();
|
|
||||||
if (exp < today) {
|
|
||||||
response = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
|
|
||||||
getToken: function() {
|
getToken: function() {
|
||||||
if ( this.isTokenValid() ) {
|
return ($rootScope.token) ? $rootScope.token : $cookieStore.get('token');
|
||||||
return ($rootScope.token) ? $rootScope.token : $cookieStore.get('token');
|
},
|
||||||
}
|
|
||||||
else {
|
|
||||||
$location.path('/login');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
retrieveToken: function(username, password) {
|
retrieveToken: function(username, password) {
|
||||||
return $http({ method: 'POST', url: '/api/v1/authtoken/',
|
return $http({ method: 'POST', url: GetBasePath('authtoken'),
|
||||||
data: {"username": username, "password": password} });
|
data: {"username": username, "password": password} });
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -70,15 +48,17 @@ angular.module('AuthService', ['ngCookies'])
|
|||||||
// should prevent content flash from the prior user.
|
// should prevent content flash from the prior user.
|
||||||
var scope = angular.element(document.getElementById('main-view')).scope();
|
var scope = angular.element(document.getElementById('main-view')).scope();
|
||||||
scope.$destroy();
|
scope.$destroy();
|
||||||
$rootScope.$destroy();
|
$rootScope.$destroy();
|
||||||
|
|
||||||
$cookieStore.remove('accordions');
|
$cookieStore.remove('accordions');
|
||||||
$cookieStore.remove('token');
|
$cookieStore.remove('token');
|
||||||
$cookieStore.remove('token_expire');
|
$cookieStore.remove('token_expire');
|
||||||
$cookieStore.remove('current_user');
|
$cookieStore.remove('current_user');
|
||||||
|
$cookieStore.put('userLoggedIn', false);
|
||||||
|
$cookieStore.put('sessionExpired', false);
|
||||||
$rootScope.current_user = {};
|
$rootScope.current_user = {};
|
||||||
$rootScope.license_tested = undefined;
|
$rootScope.license_tested = undefined;
|
||||||
$rootScope.userLoggedIn = false;
|
$rootScope.userLoggedIn = false;
|
||||||
|
$rootScope.sessionExpired = false;
|
||||||
$rootScope.token = null;
|
$rootScope.token = null;
|
||||||
$rootScope.token_expire = new Date(1970, 0, 1, 0, 0, 0, 0);
|
$rootScope.token_expire = new Date(1970, 0, 1, 0, 0, 0, 0);
|
||||||
},
|
},
|
||||||
@ -86,7 +66,7 @@ angular.module('AuthService', ['ngCookies'])
|
|||||||
getLicense: function() {
|
getLicense: function() {
|
||||||
return $http({
|
return $http({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/v1/config/',
|
url: GetBasePath('config'),
|
||||||
headers: { 'Authorization': 'Token ' + this.getToken() }
|
headers: { 'Authorization': 'Token ' + this.getToken() }
|
||||||
});
|
});
|
||||||
},
|
},
|
@ -78,8 +78,14 @@ angular.module('Utilities',[])
|
|||||||
}
|
}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
.factory('ProcessErrors', ['$log', 'Alert', function($log, Alert) {
|
.factory('ProcessErrors', ['$cookieStore', '$log', '$location', '$rootScope', 'Alert',
|
||||||
|
function($cookieStore, $log, $location, $rootScope, Alert) {
|
||||||
return function(scope, data, status, form, defaultMsg) {
|
return function(scope, data, status, form, defaultMsg) {
|
||||||
|
if ($AnsibleConfig.debug_mode && console) {
|
||||||
|
console.log('Debug status: ' + status);
|
||||||
|
console.log('Debug data: ');
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
if (status == 403) {
|
if (status == 403) {
|
||||||
var msg = 'The API responded with a 403 Access Denied error. ';
|
var msg = 'The API responded with a 403 Access Denied error. ';
|
||||||
if (data['detail']) {
|
if (data['detail']) {
|
||||||
@ -90,6 +96,11 @@ angular.module('Utilities',[])
|
|||||||
}
|
}
|
||||||
Alert(defaultMsg.hdr, msg);
|
Alert(defaultMsg.hdr, msg);
|
||||||
}
|
}
|
||||||
|
else if (status == 401 && data.detail && data.detail == 'Token is expired') {
|
||||||
|
$rootScope.sessionExpired = true;
|
||||||
|
$cookieStore.put('sessionExpired', true);
|
||||||
|
$location.path('/login');
|
||||||
|
}
|
||||||
else if (data.non_field_errors) {
|
else if (data.non_field_errors) {
|
||||||
Alert('Error!', data.non_field_errors);
|
Alert('Error!', data.non_field_errors);
|
||||||
}
|
}
|
||||||
@ -250,4 +261,3 @@ angular.module('Utilities',[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
@ -25,9 +25,9 @@
|
|||||||
<script src="{{ STATIC_URL }}js/awx-min.js"></script>
|
<script src="{{ STATIC_URL }}js/awx-min.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script src="{{ STATIC_URL }}js/app.js"></script>
|
<script src="{{ STATIC_URL }}js/app.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/authenticate.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/AuthService.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/rest-services.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/RestServices.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/utilities.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/Utilities.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/form-generator.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/form-generator.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/list-generator.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/list-generator.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/ansible/prompt-dialog.js"></script>
|
<script src="{{ STATIC_URL }}lib/ansible/prompt-dialog.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user