mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
Merge pull request #6557 from gconsidine/audic/ui-components
Audic/ui components
This commit is contained in:
commit
b4be8e833f
@ -11,7 +11,7 @@ indent_style = tab
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[**.{js,less}]
|
||||
[**.{js,less,html}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ Gruntfile.js
|
||||
karma.*.js
|
||||
|
||||
etc
|
||||
coverage
|
||||
grunt-tasks
|
||||
node_modules
|
||||
po
|
||||
@ -10,4 +11,7 @@ static
|
||||
templates
|
||||
tests
|
||||
client/**/*.js
|
||||
test
|
||||
|
||||
!client/component/**/*.js
|
||||
!client/model/**/*.js
|
||||
|
||||
@ -15,8 +15,9 @@ module.exports = {
|
||||
jsyaml: true
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 4],
|
||||
'comma-dangle': 'off',
|
||||
'prefer-const': ['off']
|
||||
indent: [0, 4],
|
||||
'comma-dangle': 0,
|
||||
'prefer-const': 0,
|
||||
'space-before-function-paren': [2, 'always']
|
||||
}
|
||||
};
|
||||
|
||||
1
awx/ui/client/features/_index.less
Normal file
1
awx/ui/client/features/_index.less
Normal file
@ -0,0 +1 @@
|
||||
@import 'credentials/_index';
|
||||
3
awx/ui/client/features/credentials/_index.less
Normal file
3
awx/ui/client/features/credentials/_index.less
Normal file
@ -0,0 +1,3 @@
|
||||
.at-CredentialsPermissions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION';
|
||||
|
||||
function AddCredentialsController (models, $state) {
|
||||
let vm = this || {};
|
||||
|
||||
let me = models.me;
|
||||
let credential = models.credential;
|
||||
let credentialType = models.credentialType;
|
||||
let organization = models.organization;
|
||||
|
||||
vm.panelTitle = 'NEW CREDENTIAL';
|
||||
|
||||
vm.tab = {
|
||||
details: {
|
||||
_active: true
|
||||
},
|
||||
permissions:{
|
||||
_disabled: true
|
||||
}
|
||||
};
|
||||
|
||||
vm.form = credential.createFormSchema('post', {
|
||||
omit: ['user', 'team', 'inputs']
|
||||
});
|
||||
|
||||
vm.form.organization._placeholder = DEFAULT_ORGANIZATION_PLACEHOLDER;
|
||||
vm.form.organization._data = organization.get('results');
|
||||
vm.form.organization._format = 'objects';
|
||||
vm.form.organization._exp = 'org as org.name for org in state._data';
|
||||
vm.form.organization._display = 'name';
|
||||
vm.form.organization._key = 'id';
|
||||
|
||||
vm.form.credential_type._data = credentialType.get('results');
|
||||
vm.form.credential_type._placeholder = 'SELECT A TYPE';
|
||||
vm.form.credential_type._format = 'grouped-object';
|
||||
vm.form.credential_type._display = 'name';
|
||||
vm.form.credential_type._key = 'id';
|
||||
vm.form.credential_type._exp = 'type as type.name group by type.kind for type in state._data';
|
||||
|
||||
vm.form.inputs = {
|
||||
_get: credentialType.mergeInputProperties,
|
||||
_source: vm.form.credential_type,
|
||||
_reference: 'vm.form.inputs',
|
||||
_key: 'inputs'
|
||||
};
|
||||
|
||||
vm.form.save = data => {
|
||||
data.user = me.getSelf().id;
|
||||
|
||||
return credential.request('post', data);
|
||||
};
|
||||
|
||||
vm.form.onSaveSuccess = res => {
|
||||
$state.go('credentials.edit', { credential_id: res.data.id }, { reload: true });
|
||||
};
|
||||
}
|
||||
|
||||
AddCredentialsController.$inject = [
|
||||
'resolvedModels',
|
||||
'$state'
|
||||
];
|
||||
|
||||
export default AddCredentialsController;
|
||||
@ -0,0 +1,46 @@
|
||||
<at-panel ng-if="$state.current.name === 'credentials.add' || $state.current.name === 'credentials.edit'">
|
||||
<at-panel-heading>{{ vm.panelTitle }}</at-panel-heading>
|
||||
|
||||
<at-tab-group>
|
||||
<at-tab state="vm.tab.details">Details</at-tab>
|
||||
<at-tab state="vm.tab.permissions">Permissions</at-tab>
|
||||
</at-tab-group>
|
||||
|
||||
<at-panel-body>
|
||||
<at-form state="vm.form">
|
||||
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
|
||||
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
|
||||
<at-input-select col="4" tab="3" state="vm.form.organization"></at-input-select>
|
||||
|
||||
<at-divider></at-divider>
|
||||
|
||||
<at-input-select col="4" tab="4" state="vm.form.credential_type"></at-input-select>
|
||||
|
||||
<at-input-group col="4" tab="4" state="vm.form.inputs">
|
||||
Type Details
|
||||
</at-input-group>
|
||||
|
||||
<at-action-group col="12" pos="right">
|
||||
<at-form-action type="cancel"></at-form-action>
|
||||
<at-form-action type="save"></at-form-action>
|
||||
</at-action-group>
|
||||
</at-form>
|
||||
</at-panel-body>
|
||||
</at-panel>
|
||||
|
||||
<at-panel ng-if="$state.current.name === 'credentials.edit.permissions' ||
|
||||
$state.current.name === 'credentials.edit.permissions.add'">
|
||||
<at-panel-heading>Credentials Permissions</at-panel-heading>
|
||||
|
||||
<at-tab-group>
|
||||
<at-tab state="vm.tab.details">Details</at-tab>
|
||||
<at-tab state="vm.tab.permissions">Permissions</at-tab>
|
||||
</at-tab-group>
|
||||
|
||||
<at-panel-body>
|
||||
<div class="at-CredentialsPermissions" ui-view="related"></div>
|
||||
</at-panel-body>
|
||||
</at-panel>
|
||||
|
||||
<div ng-if="$state.current.name === 'credentials.edit.permissions.add'" ui-view="modal"></div>
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION';
|
||||
|
||||
function EditCredentialsController (models, $state, $scope) {
|
||||
let vm = this || {};
|
||||
|
||||
let me = models.me;
|
||||
let credential = models.credential;
|
||||
let credentialType = models.credentialType;
|
||||
let organization = models.organization;
|
||||
|
||||
vm.tab = {
|
||||
details: {
|
||||
_active: true,
|
||||
_go: 'credentials.edit',
|
||||
_params: { credential_id: credential.get('id') }
|
||||
},
|
||||
permissions:{
|
||||
_go: 'credentials.edit.permissions',
|
||||
_params: { credential_id: credential.get('id') }
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('$state.current.name', (value) => {
|
||||
if (value === 'credentials.edit') {
|
||||
vm.tab.details._active = true;
|
||||
vm.tab.details._permissions = false;
|
||||
} else {
|
||||
vm.tab.permissions._active = true;
|
||||
vm.tab.details._active = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Only exists for permissions compatibility
|
||||
$scope.credential_obj = credential.get();
|
||||
|
||||
vm.panelTitle = credential.get('name');
|
||||
|
||||
vm.form = credential.createFormSchema('put', {
|
||||
omit: ['user', 'team', 'inputs']
|
||||
});
|
||||
|
||||
vm.form.organization._placeholder = DEFAULT_ORGANIZATION_PLACEHOLDER;
|
||||
vm.form.organization._data = organization.get('results');
|
||||
vm.form.organization._format = 'objects';
|
||||
vm.form.organization._exp = 'org as org.name for org in state._data';
|
||||
vm.form.organization._display = 'name';
|
||||
vm.form.organization._key = 'id';
|
||||
vm.form.organization._value = organization.getById(credential.get('organization'));
|
||||
|
||||
vm.form.credential_type._data = credentialType.get('results');
|
||||
vm.form.credential_type._format = 'grouped-object';
|
||||
vm.form.credential_type._display = 'name';
|
||||
vm.form.credential_type._key = 'id';
|
||||
vm.form.credential_type._exp = 'type as type.name group by type.kind for type in state._data';
|
||||
vm.form.credential_type._value = credentialType.getById(credential.get('credential_type'));
|
||||
|
||||
vm.form.inputs = {
|
||||
_get (type) {
|
||||
let inputs = credentialType.mergeInputProperties(type);
|
||||
|
||||
if (type.id === credential.get('credential_type')) {
|
||||
inputs = credential.assignInputGroupValues(inputs);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
},
|
||||
_source: vm.form.credential_type,
|
||||
_reference: 'vm.form.inputs',
|
||||
_key: 'inputs'
|
||||
};
|
||||
|
||||
vm.form.save = data => {
|
||||
data.user = me.getSelf().id;
|
||||
credential.clearTypeInputs();
|
||||
|
||||
return credential.request('put', data);
|
||||
};
|
||||
|
||||
vm.form.onSaveSuccess = res => {
|
||||
$state.go('credentials', { reload: true });
|
||||
};
|
||||
}
|
||||
|
||||
EditCredentialsController.$inject = [
|
||||
'resolvedModels',
|
||||
'$state',
|
||||
'$scope'
|
||||
];
|
||||
|
||||
export default EditCredentialsController;
|
||||
283
awx/ui/client/features/credentials/index.js
Normal file
283
awx/ui/client/features/credentials/index.js
Normal file
@ -0,0 +1,283 @@
|
||||
import PermissionsList from '../../src/access/permissions-list.controller';
|
||||
import CredentialForm from '../../src/credentials/credentials.form';
|
||||
import CredentialList from '../../src/credentials/credentials.list';
|
||||
import ListController from '../../src/credentials/list/credentials-list.controller';
|
||||
import AddController from './add-credentials.controller.js';
|
||||
import EditController from './edit-credentials.controller.js';
|
||||
import { N_ } from '../../src/i18n';
|
||||
|
||||
function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) {
|
||||
let id = $stateParams.credential_id;
|
||||
|
||||
let promises = {
|
||||
me: new Me('get'),
|
||||
credentialType: new CredentialType('get'),
|
||||
organization: new Organization('get')
|
||||
};
|
||||
|
||||
if (id) {
|
||||
promises.credential = new Credential(['get', 'options'], [id, id]);
|
||||
} else {
|
||||
promises.credential = new Credential('options');
|
||||
}
|
||||
|
||||
return $q.all(promises);
|
||||
}
|
||||
|
||||
CredentialsResolve.$inject = [
|
||||
'$q',
|
||||
'$stateParams',
|
||||
'MeModel',
|
||||
'CredentialModel',
|
||||
'CredentialTypeModel',
|
||||
'OrganizationModel'
|
||||
];
|
||||
|
||||
function CredentialsConfig ($stateProvider, $stateExtenderProvider, pathServiceProvider) {
|
||||
let pathService = pathServiceProvider.$get();
|
||||
let stateExtender = $stateExtenderProvider.$get();
|
||||
|
||||
stateExtender.addState({
|
||||
name: 'credentials',
|
||||
route: '/credentials',
|
||||
ncyBreadcrumb: {
|
||||
label: N_('CREDENTIALS')
|
||||
},
|
||||
views: {
|
||||
'@': {
|
||||
templateUrl: pathService.getViewPath('credentials/index')
|
||||
},
|
||||
'list@credentials': {
|
||||
templateProvider: function(CredentialList, generateList) {
|
||||
let html = generateList.build({
|
||||
list: CredentialList,
|
||||
mode: 'edit'
|
||||
});
|
||||
|
||||
return html;
|
||||
},
|
||||
controller: ListController
|
||||
}
|
||||
},
|
||||
searchPrefix: 'credential',
|
||||
resolve: {
|
||||
Dataset: ['CredentialList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
function(list, qs, $stateParams, GetBasePath) {
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
stateExtender.addState({
|
||||
name: 'credentials.add',
|
||||
route: '/add',
|
||||
ncyBreadcrumb: {
|
||||
label: N_('CREATE CREDENTIALS')
|
||||
},
|
||||
views: {
|
||||
'add@credentials': {
|
||||
templateUrl: pathService.getViewPath('credentials/add-edit-credentials'),
|
||||
controller: AddController,
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
resolvedModels: CredentialsResolve
|
||||
}
|
||||
});
|
||||
|
||||
stateExtender.addState({
|
||||
name: 'credentials.edit',
|
||||
route: '/:credential_id',
|
||||
ncyBreadcrumb: {
|
||||
label: N_('EDIT')
|
||||
},
|
||||
views: {
|
||||
'edit@credentials': {
|
||||
templateUrl: pathService.getViewPath('credentials/add-edit-credentials'),
|
||||
controller: EditController,
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
resolvedModels: CredentialsResolve
|
||||
}
|
||||
});
|
||||
|
||||
stateExtender.addState({
|
||||
name: "credentials.edit.permissions",
|
||||
url: "/permissions?{permission_search:queryset}",
|
||||
resolve: {
|
||||
ListDefinition: () => {
|
||||
return {
|
||||
name: 'permissions',
|
||||
disabled: '(organization === undefined ? true : false)',
|
||||
// Do not transition the state if organization is undefined
|
||||
ngClick: `(organization === undefined ? true : false)||$state.go('credentials.edit.permissions')`,
|
||||
awToolTip: '{{permissionsTooltip}}',
|
||||
dataTipWatch: 'permissionsTooltip',
|
||||
awToolTipTabEnabledInEditMode: true,
|
||||
dataPlacement: 'right',
|
||||
basePath: 'api/v2/credentials/{{$stateParams.id}}/access_list/',
|
||||
search: {
|
||||
order_by: 'username'
|
||||
},
|
||||
type: 'collection',
|
||||
title: N_('Permissions'),
|
||||
iterator: 'permission',
|
||||
index: false,
|
||||
open: false,
|
||||
actions: {
|
||||
add: {
|
||||
ngClick: "$state.go('.add')",
|
||||
label: 'Add',
|
||||
awToolTip: N_('Add a permission'),
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ' + N_('ADD'),
|
||||
ngShow: '(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
username: {
|
||||
key: true,
|
||||
label: N_('User'),
|
||||
linkBase: 'users',
|
||||
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
|
||||
},
|
||||
role: {
|
||||
label: N_('Role'),
|
||||
type: 'role',
|
||||
nosort: true,
|
||||
class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4'
|
||||
},
|
||||
team_roles: {
|
||||
label: N_('Team Roles'),
|
||||
type: 'team_roles',
|
||||
nosort: true,
|
||||
class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4'
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
Dataset: ['QuerySet', '$stateParams', (qs, $stateParams) => {
|
||||
let id = $stateParams.credential_id;
|
||||
let path = `api/v2/credentials/${id}/access_list/`;
|
||||
|
||||
return qs.search(path, $stateParams[`permission_search`]);
|
||||
}
|
||||
]
|
||||
},
|
||||
params: {
|
||||
permission_search: {
|
||||
value: {
|
||||
page_size: "20",
|
||||
order_by: "username"
|
||||
},
|
||||
dynamic:true,
|
||||
squash:""
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: "credentials.edit",
|
||||
label: "PERMISSIONS"
|
||||
},
|
||||
views: {
|
||||
'related': {
|
||||
templateProvider: function(CredentialForm, GenerateForm) {
|
||||
let html = GenerateForm.buildCollection({
|
||||
mode: 'edit',
|
||||
related: `permissions`,
|
||||
form: typeof(CredentialForm) === 'function' ?
|
||||
CredentialForm() : CredentialForm
|
||||
});
|
||||
return html;
|
||||
},
|
||||
controller: 'PermissionsList'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stateExtender.addState({
|
||||
name: 'credentials.edit.permissions.add',
|
||||
url: '/add-permissions',
|
||||
resolve: {
|
||||
usersDataset: [
|
||||
'addPermissionsUsersList',
|
||||
'QuerySet',
|
||||
'$stateParams',
|
||||
'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
return qs.search(path, $stateParams.user_search);
|
||||
|
||||
}
|
||||
],
|
||||
teamsDataset: [
|
||||
'addPermissionsTeamsList',
|
||||
'QuerySet',
|
||||
'$stateParams',
|
||||
'GetBasePath',
|
||||
(list, qs, $stateParams, GetBasePath) => {
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
return qs.search(path, $stateParams.team_search);
|
||||
}
|
||||
],
|
||||
resourceData: ['CredentialModel', '$stateParams', (Credential, $stateParams) => {
|
||||
return new Credential('get', $stateParams.credential_id)
|
||||
.then(credential => ({ data: credential.get() }));
|
||||
}],
|
||||
},
|
||||
params: {
|
||||
user_search: {
|
||||
value: {
|
||||
order_by: 'username',
|
||||
page_size: 5
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
team_search: {
|
||||
value: {
|
||||
order_by: 'name',
|
||||
page_size: 5
|
||||
},
|
||||
dynamic: true
|
||||
}
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
views: {
|
||||
'modal@credentials.edit': {
|
||||
template: `
|
||||
<add-rbac-resource
|
||||
users-dataset="$resolve.usersDataset"
|
||||
teams-dataset="$resolve.teamsDataset"
|
||||
selected="allSelected"
|
||||
resource-data="$resolve.resourceData"
|
||||
title="Add Users / Teams">
|
||||
</add-rbac-resource>`
|
||||
}
|
||||
},
|
||||
onExit: $state => {
|
||||
if ($state.transition) {
|
||||
$('#add-permissions-modal').modal('hide');
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CredentialsConfig.$inject = [
|
||||
'$stateProvider',
|
||||
'$stateExtenderProvider',
|
||||
'PathServiceProvider'
|
||||
];
|
||||
|
||||
angular
|
||||
.module('at.features.credentials', [])
|
||||
.config(CredentialsConfig)
|
||||
.controller('AddController', AddController)
|
||||
.controller('EditController', EditController);
|
||||
6
awx/ui/client/features/credentials/index.view.html
Normal file
6
awx/ui/client/features/credentials/index.view.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div ui-view="edit"></div>
|
||||
<div ui-view="add"></div>
|
||||
|
||||
<div class="panel at-Panel">
|
||||
<div ui-view="list"></div>
|
||||
</div>
|
||||
5
awx/ui/client/features/index.js
Normal file
5
awx/ui/client/features/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import './credentials';
|
||||
|
||||
angular.module('at.features', [
|
||||
'at.features.credentials'
|
||||
]);
|
||||
7
awx/ui/client/lib/components/_index.less
Normal file
7
awx/ui/client/lib/components/_index.less
Normal file
@ -0,0 +1,7 @@
|
||||
@import 'action/_index';
|
||||
@import 'input/_index';
|
||||
@import 'panel/_index';
|
||||
@import 'modal/_index';
|
||||
@import 'popover/_index';
|
||||
@import 'tabs/_index';
|
||||
@import 'utility/_index';
|
||||
7
awx/ui/client/lib/components/action/_index.less
Normal file
7
awx/ui/client/lib/components/action/_index.less
Normal file
@ -0,0 +1,7 @@
|
||||
.at-ActionGroup {
|
||||
margin-top: @at-space-6x;
|
||||
|
||||
button:last-child {
|
||||
margin-left: @at-space-5x;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
function atActionGroup (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
templateUrl: pathService.getPartialPath('components/action/action-group'),
|
||||
scope: {
|
||||
col: '@',
|
||||
pos: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atActionGroup.$inject = ['PathService'];
|
||||
|
||||
export default atActionGroup;
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="col-sm-{{::col }} at-ActionGroup">
|
||||
<div class="pull-{{::pos }}">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
74
awx/ui/client/lib/components/form/action.directive.js
Normal file
74
awx/ui/client/lib/components/form/action.directive.js
Normal file
@ -0,0 +1,74 @@
|
||||
function link (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let actionController = controllers[1];
|
||||
|
||||
actionController.init(formController, element, scope);
|
||||
}
|
||||
|
||||
function atFormActionController ($state) {
|
||||
let vm = this || {};
|
||||
|
||||
let element;
|
||||
let form;
|
||||
let scope;
|
||||
|
||||
vm.init = (_form_, _element_, _scope_) => {
|
||||
form = _form_;
|
||||
element = _element_;
|
||||
scope = _scope_;
|
||||
|
||||
switch(scope.type) {
|
||||
case 'cancel':
|
||||
vm.setCancelDefaults();
|
||||
break;
|
||||
case 'save':
|
||||
vm.setSaveDefaults();
|
||||
break;
|
||||
default:
|
||||
vm.setCustomDefaults();
|
||||
}
|
||||
|
||||
form.register('action', scope);
|
||||
};
|
||||
|
||||
vm.setCustomDefaults = () => {
|
||||
|
||||
};
|
||||
|
||||
vm.setCancelDefaults = () => {
|
||||
scope.text = 'CANCEL';
|
||||
scope.fill = 'Hollow';
|
||||
scope.color = 'white';
|
||||
scope.action = () => $state.go('^');
|
||||
};
|
||||
|
||||
vm.setSaveDefaults = () => {
|
||||
scope.text = 'SAVE';
|
||||
scope.fill = '';
|
||||
scope.color = 'green';
|
||||
scope.action = () => form.submit();
|
||||
};
|
||||
}
|
||||
|
||||
atFormAction.$inject = ['$state'];
|
||||
|
||||
function atFormAction (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atFormAction'],
|
||||
templateUrl: pathService.getPartialPath('components/form/action'),
|
||||
controller: atFormActionController,
|
||||
controllerAs: 'vm',
|
||||
link,
|
||||
scope: {
|
||||
state: '=',
|
||||
type: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atFormAction.$inject = ['PathService'];
|
||||
|
||||
export default atFormAction;
|
||||
5
awx/ui/client/lib/components/form/action.partial.html
Normal file
5
awx/ui/client/lib/components/form/action.partial.html
Normal file
@ -0,0 +1,5 @@
|
||||
<button class="btn at-Button{{ fill }}--{{ color }}"
|
||||
ng-disabled="form.disabled || (type === 'save' && !form.isValid)"
|
||||
ng-click="action()">
|
||||
{{::text}}
|
||||
</button>
|
||||
209
awx/ui/client/lib/components/form/form.directive.js
Normal file
209
awx/ui/client/lib/components/form/form.directive.js
Normal file
@ -0,0 +1,209 @@
|
||||
function atFormLink (scope, el, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let form = el[0];
|
||||
|
||||
formController.init(scope, form);
|
||||
}
|
||||
|
||||
function AtFormController (eventService) {
|
||||
let vm = this || {};
|
||||
|
||||
let scope;
|
||||
let form;
|
||||
|
||||
vm.components = [];
|
||||
vm.modal = {};
|
||||
vm.state = {
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
value: {},
|
||||
};
|
||||
|
||||
vm.init = (_scope_, _form_) => {
|
||||
scope = _scope_;
|
||||
form = _form_;
|
||||
|
||||
vm.setListeners();
|
||||
};
|
||||
|
||||
vm.register = (category, component, el) => {
|
||||
component.category = category;
|
||||
component.form = vm.state;
|
||||
|
||||
vm.components.push(component)
|
||||
};
|
||||
|
||||
vm.setListeners = () => {
|
||||
let listeners = eventService.addListeners([
|
||||
[form, 'keypress', vm.submitOnEnter]
|
||||
]);
|
||||
|
||||
scope.$on('$destroy', () => eventService.remove(listeners));
|
||||
};
|
||||
|
||||
vm.submitOnEnter = event => {
|
||||
if (event.key !== 'Enter' || event.srcElement.type === 'textarea') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
scope.$apply(vm.submit);
|
||||
};
|
||||
|
||||
vm.submit = event => {
|
||||
if (!vm.state.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
vm.state.disabled = true;
|
||||
|
||||
let data = vm.components
|
||||
.filter(component => component.category === 'input')
|
||||
.reduce((values, component) => {
|
||||
if (!component.state._value) {
|
||||
return values;
|
||||
}
|
||||
|
||||
if (component.state._key && typeof component.state._value === 'object') {
|
||||
values[component.state.id] = component.state._value[component.state._key];
|
||||
} else if (component.state._group) {
|
||||
values[component.state._key] = values[component.state._key] || {};
|
||||
values[component.state._key][component.state.id] = component.state._value;
|
||||
} else {
|
||||
values[component.state.id] = component.state._value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}, {});
|
||||
|
||||
scope.state.save(data)
|
||||
.then(scope.state.onSaveSuccess)
|
||||
.catch(err => vm.onSaveError(err))
|
||||
.finally(() => vm.state.disabled = false);
|
||||
};
|
||||
|
||||
vm.onSaveError = err => {
|
||||
let handled;
|
||||
|
||||
if (err.status === 400) {
|
||||
handled = vm.handleValidationError(err.data);
|
||||
}
|
||||
|
||||
if (err.status === 500) {
|
||||
handled = vm.handleUnexpectedError(err);
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
let message;
|
||||
|
||||
if (typeof err.data === 'object') {
|
||||
message = JSON.stringify(err.data);
|
||||
} else {
|
||||
message = err.data;
|
||||
}
|
||||
|
||||
vm.modal.show('Unable to Submit', `Unexpected Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
vm.handleUnexpectedError = err => {
|
||||
let title = 'Unable to Submit';
|
||||
let message = 'Unexpected server error. View the console for more information';
|
||||
|
||||
vm.modal.show(title, message);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
vm.handleValidationError = errors => {
|
||||
let errorMessageSet = vm.setValidationMessages(errors);
|
||||
|
||||
if (errorMessageSet) {
|
||||
vm.check();
|
||||
}
|
||||
|
||||
return errorMessageSet;
|
||||
};
|
||||
|
||||
vm.setValidationMessages = (errors, errorSet) => {
|
||||
let errorMessageSet = errorSet || false;
|
||||
|
||||
for (let id in errors) {
|
||||
if (!Array.isArray(errors[id]) && typeof errors[id] === 'object') {
|
||||
errorMessageSet = vm.setValidationMessages(errors[id], errorMessageSet);
|
||||
continue;
|
||||
}
|
||||
|
||||
vm.components
|
||||
.filter(component => component.category === 'input')
|
||||
.filter(component => errors[component.state.id])
|
||||
.forEach(component => {
|
||||
errorMessageSet = true;
|
||||
|
||||
component.state._rejected = true;
|
||||
component.state._isValid = false;
|
||||
component.state._message = errors[component.state.id].join(' ');
|
||||
});
|
||||
}
|
||||
|
||||
return errorMessageSet;
|
||||
};
|
||||
|
||||
vm.validate = () => {
|
||||
let isValid = true;
|
||||
|
||||
for (let i = 0; i < vm.components.length; i++) {
|
||||
if (vm.components[i].category !== 'input') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!vm.components[i].state._isValid) {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
vm.check = () => {
|
||||
let isValid = vm.validate();
|
||||
|
||||
if (isValid !== vm.state.isValid) {
|
||||
vm.state.isValid = isValid;
|
||||
}
|
||||
};
|
||||
|
||||
vm.deregisterInputGroup = components => {
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
for (let j = 0; j < vm.components.length; j++) {
|
||||
if (components[i] === vm.components[j].state) {
|
||||
vm.components.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AtFormController.$inject = ['EventService'];
|
||||
|
||||
function atForm (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
require: ['atForm'],
|
||||
templateUrl: pathService.getPartialPath('components/form/form'),
|
||||
controller: AtFormController,
|
||||
controllerAs: 'vm',
|
||||
link: atFormLink,
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atForm.$inject = ['PathService'];
|
||||
|
||||
export default atForm;
|
||||
9
awx/ui/client/lib/components/form/form.partial.html
Normal file
9
awx/ui/client/lib/components/form/form.partial.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div>
|
||||
<form>
|
||||
<div class="row">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<at-modal state="vm.modal"></at-modal>
|
||||
</div>
|
||||
52
awx/ui/client/lib/components/index.js
Normal file
52
awx/ui/client/lib/components/index.js
Normal file
@ -0,0 +1,52 @@
|
||||
import actionGroup from './action/action-group.directive';
|
||||
import divider from './utility/divider.directive';
|
||||
import form from './form/form.directive';
|
||||
import formAction from './form/action.directive';
|
||||
import inputCheckbox from './input/checkbox.directive';
|
||||
import inputGroup from './input/group.directive';
|
||||
import inputLabel from './input/label.directive';
|
||||
import inputLookup from './input/lookup.directive';
|
||||
import inputMessage from './input/message.directive';
|
||||
import inputNumber from './input/number.directive';
|
||||
import inputSelect from './input/select.directive';
|
||||
import inputSecret from './input/secret.directive';
|
||||
import inputText from './input/text.directive';
|
||||
import inputTextarea from './input/textarea.directive';
|
||||
import inputTextareaSecret from './input/textarea-secret.directive';
|
||||
import modal from './modal/modal.directive';
|
||||
import panel from './panel/panel.directive';
|
||||
import panelHeading from './panel/heading.directive';
|
||||
import panelBody from './panel/body.directive';
|
||||
import popover from './popover/popover.directive';
|
||||
import tab from './tabs/tab.directive';
|
||||
import tabGroup from './tabs/group.directive';
|
||||
|
||||
import BaseInputController from './input/base.controller';
|
||||
|
||||
angular
|
||||
.module('at.lib.components', [])
|
||||
.directive('atActionGroup', actionGroup)
|
||||
.directive('atDivider', divider)
|
||||
.directive('atForm', form)
|
||||
.directive('atFormAction', formAction)
|
||||
.directive('atInputCheckbox', inputCheckbox)
|
||||
.directive('atInputGroup', inputGroup)
|
||||
.directive('atInputLabel', inputLabel)
|
||||
.directive('atInputLookup', inputLookup)
|
||||
.directive('atInputMessage', inputMessage)
|
||||
.directive('atInputNumber', inputNumber)
|
||||
.directive('atInputSecret', inputSecret)
|
||||
.directive('atInputSelect', inputSelect)
|
||||
.directive('atInputText', inputText)
|
||||
.directive('atInputTextarea', inputTextarea)
|
||||
.directive('atInputTextareaSecret', inputTextareaSecret)
|
||||
.directive('atModal', modal)
|
||||
.directive('atPanel', panel)
|
||||
.directive('atPanelHeading', panelHeading)
|
||||
.directive('atPanelBody', panelBody)
|
||||
.directive('atPopover', popover)
|
||||
.directive('atTab', tab)
|
||||
.directive('atTabGroup', tabGroup)
|
||||
.service('BaseInputController', BaseInputController);
|
||||
|
||||
|
||||
208
awx/ui/client/lib/components/input/_index.less
Normal file
208
awx/ui/client/lib/components/input/_index.less
Normal file
@ -0,0 +1,208 @@
|
||||
.at-Input {
|
||||
.at-mixin-Placeholder(@at-gray-dark-3x);
|
||||
|
||||
height: @at-input-height;
|
||||
background: @at-white;
|
||||
border-radius: @at-border-radius;
|
||||
color: @at-gray-dark-5x;
|
||||
|
||||
&, &:active {
|
||||
border-color: @at-gray-dark-2x;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: @at-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputCheckbox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
& > label {
|
||||
& > input[type=checkbox] {
|
||||
height: @at-input-height;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin: 0;
|
||||
padding: 0 0 0 @at-space-6x;
|
||||
line-height: @at-line-height-tall;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputContainer {
|
||||
margin-top: @at-space-6x;
|
||||
}
|
||||
|
||||
.at-Input-button {
|
||||
min-width: @at-input-button-width;
|
||||
display: block;
|
||||
height: @at-input-height;
|
||||
|
||||
&, &:active, &:hover, &:focus {
|
||||
color: @at-gray-dark-3x;
|
||||
border-color: @at-gray-dark-2x;
|
||||
background-color: @at-white;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.at-Input--focus {
|
||||
border-color: @at-blue;
|
||||
}
|
||||
|
||||
.at-Input--rejected {
|
||||
&, &:focus {
|
||||
border-color: @at-red;
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputFile--hidden {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: @at-input-button-width;
|
||||
z-index: -2;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.at-InputFile--drag {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.at-InputGroup {
|
||||
padding: 0;
|
||||
margin: @at-space-6x 0 0 0;
|
||||
}
|
||||
|
||||
.at-InputGroup-border {
|
||||
position: absolute;
|
||||
width: @at-inset-width;
|
||||
height: 100%;
|
||||
background: @at-gray-dark;
|
||||
left: -@at-inset-width;
|
||||
}
|
||||
|
||||
.at-InputGroup-button {
|
||||
height: 100%;
|
||||
|
||||
& > button {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputGroup-title {
|
||||
.at-mixin-Heading(@at-font-size-2x);
|
||||
margin: 0 0 0 @at-space-5x;
|
||||
}
|
||||
|
||||
.at-InputGroup-divider {
|
||||
clear: both;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: @at-space-6x;
|
||||
}
|
||||
|
||||
.at-InputLabel {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.at-InputLabel-name {
|
||||
color: @at-gray-dark-4x;
|
||||
font-size: @at-font-size-2x;
|
||||
font-weight: @at-font-weight;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.at-InputLabel-hint {
|
||||
margin-left: @at-space-4x;
|
||||
color: @at-gray-dark-3x;
|
||||
font-size: @at-font-size;
|
||||
font-weight: @at-font-weight;
|
||||
line-height: @at-line-height-short;
|
||||
}
|
||||
|
||||
.at-InputLabel-checkbox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-InputLabel-checkboxLabel {
|
||||
margin-bottom: 0;
|
||||
|
||||
& > input[type=checkbox] {
|
||||
margin: 0 @at-space 0 0;
|
||||
position: relative;
|
||||
top: @at-space;
|
||||
}
|
||||
|
||||
& > p {
|
||||
font-size: @at-font-size;
|
||||
color: @at-gray-dark-4x;
|
||||
font-weight: @at-font-weight;
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputMessage--rejected {
|
||||
font-size: @at-font-size;
|
||||
color: @at-red;
|
||||
margin: @at-space-3x 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-InputLabel-required {
|
||||
color: @at-red;
|
||||
font-weight: @at-font-weight-2x;
|
||||
font-size: @at-font-size-2x;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.at-InputSelect {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
& > i {
|
||||
font-size: @at-font-size;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
top: @at-space-4x;
|
||||
right: @at-space-4x;
|
||||
color: @at-gray-dark-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputSelect-input {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.at-InputSelect-select {
|
||||
height: @at-input-height;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
|
||||
& > optgroup {
|
||||
text-transform: uppercase;
|
||||
|
||||
& > option {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.at-InputTextarea {
|
||||
.at-mixin-FontFixedWidth();
|
||||
}
|
||||
121
awx/ui/client/lib/components/input/base.controller.js
Normal file
121
awx/ui/client/lib/components/input/base.controller.js
Normal file
@ -0,0 +1,121 @@
|
||||
const REQUIRED_INPUT_MISSING_MESSAGE = 'Please enter a value.';
|
||||
const DEFAULT_INVALID_INPUT_MESSAGE = 'Invalid input for this type.';
|
||||
const PROMPT_ON_LAUNCH_VALUE = 'ASK';
|
||||
const ENCRYPTED_VALUE = '$encrypted$';
|
||||
|
||||
function BaseInputController () {
|
||||
return function extend (type, scope, element, form) {
|
||||
let vm = this;
|
||||
|
||||
scope.state = scope.state || {};
|
||||
|
||||
scope.state._required = scope.state.required || false;
|
||||
scope.state._isValid = scope.state.isValid || false;
|
||||
scope.state._disabled = scope.state.disabled || false;
|
||||
scope.state._activeModel = '_value';
|
||||
|
||||
if (scope.state.ask_at_runtime) {
|
||||
scope.state._displayPromptOnLaunch = true;
|
||||
}
|
||||
|
||||
if (scope.state._value) {
|
||||
scope.state._edit = true;
|
||||
scope.state._preEditValue = scope.state._value;
|
||||
|
||||
if (scope.state._value === PROMPT_ON_LAUNCH_VALUE) {
|
||||
scope.state._promptOnLaunch = true;
|
||||
scope.state._disabled = true;
|
||||
scope.state._activeModel = '_displayValue';
|
||||
}
|
||||
|
||||
if (scope.state._value === ENCRYPTED_VALUE) {
|
||||
scope.state._displayRevertReplace = true;
|
||||
scope.state._enableToggle = true;
|
||||
scope.state._disabled = true;
|
||||
scope.state._isBeingReplaced = false;
|
||||
scope.state._activeModel = '_displayValue';
|
||||
}
|
||||
}
|
||||
|
||||
form.register(type, scope);
|
||||
|
||||
vm.validate = () => {
|
||||
let isValid = true;
|
||||
let message = '';
|
||||
|
||||
if (scope.state._required && !scope.state._value) {
|
||||
isValid = false;
|
||||
message = REQUIRED_INPUT_MISSING_MESSAGE;
|
||||
}
|
||||
|
||||
if (scope.state.validate) {
|
||||
let result = scope.state._validate(scope.state._value);
|
||||
|
||||
if (!result.isValid) {
|
||||
isValid = false;
|
||||
message = result.message || DEFAULT_INVALID_INPUT_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid,
|
||||
message
|
||||
};
|
||||
};
|
||||
|
||||
vm.check = () => {
|
||||
let result = vm.validate();
|
||||
|
||||
if (result.isValid !== scope.state._isValid) {
|
||||
scope.state._rejected = !result.isValid;
|
||||
scope.state._isValid = result.isValid;
|
||||
scope.state._message = result.message;
|
||||
|
||||
form.check();
|
||||
}
|
||||
};
|
||||
|
||||
vm.toggleRevertReplace = () => {
|
||||
scope.state._isBeingReplaced = !scope.state._isBeingReplaced;
|
||||
|
||||
if (!scope.state._isBeingReplaced) {
|
||||
scope.state._buttonText = 'REPLACE';
|
||||
scope.state._disabled = true;
|
||||
scope.state._enableToggle = true;
|
||||
scope.state._value = scope.state._preEditValue;
|
||||
scope.state._activeModel = '_displayValue';
|
||||
scope.state._placeholder = 'ENCRYPTED';
|
||||
} else {
|
||||
scope.state._buttonText = 'REVERT';
|
||||
scope.state._disabled = false;
|
||||
scope.state._enableToggle = false;
|
||||
scope.state._activeModel = '_value';
|
||||
scope.state._value = '';
|
||||
scope.state._placeholder = '';
|
||||
}
|
||||
};
|
||||
|
||||
vm.togglePromptOnLaunch = () => {
|
||||
if (scope.state._promptOnLaunch) {
|
||||
scope.state._value = PROMPT_ON_LAUNCH_VALUE;
|
||||
scope.state._activeModel = '_displayValue';
|
||||
scope.state._disabled = true;
|
||||
scope.state._enableToggle = false;
|
||||
} else {
|
||||
if (scope.state._isBeingReplaced === false) {
|
||||
scope.state._disabled = true;
|
||||
scope.state._enableToggle = true;
|
||||
scope.state._value = scope.state._preEditValue;
|
||||
} else {
|
||||
scope.state._activeModel = '_value';
|
||||
scope.state._disabled = false;
|
||||
scope.state._value = '';
|
||||
}
|
||||
}
|
||||
|
||||
vm.check();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default BaseInputController;
|
||||
46
awx/ui/client/lib/components/input/checkbox.directive.js
Normal file
46
awx/ui/client/lib/components/input/checkbox.directive.js
Normal file
@ -0,0 +1,46 @@
|
||||
function atInputCheckboxLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputCheckboxController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
vm.init = (scope, element, form) => {
|
||||
baseInputController.call(vm, 'input', scope, element, form);
|
||||
scope.label = scope.state.label;
|
||||
scope.state.label = 'OPTIONS';
|
||||
|
||||
vm.check();
|
||||
};
|
||||
}
|
||||
|
||||
AtInputCheckboxController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputCheckbox (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputCheckbox'],
|
||||
templateUrl: pathService.getPartialPath('components/input/checkbox'),
|
||||
controller: AtInputCheckboxController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputCheckboxLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputCheckbox.$inject = ['PathService'];
|
||||
|
||||
export default atInputCheckbox;
|
||||
17
awx/ui/client/lib/components/input/checkbox.partial.html
Normal file
17
awx/ui/client/lib/components/input/checkbox.partial.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
<div class="checkbox at-InputCheckbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-model="state._value"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
<p>{{ label }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
185
awx/ui/client/lib/components/input/group.directive.js
Normal file
185
awx/ui/client/lib/components/input/group.directive.js
Normal file
@ -0,0 +1,185 @@
|
||||
function atInputGroupLink (scope, el, attrs, controllers) {
|
||||
let groupController = controllers[0];
|
||||
let formController = controllers[1];
|
||||
let element = el[0].getElementsByClassName('at-InputGroup-container')[0];
|
||||
|
||||
groupController.init(scope, formController, element);
|
||||
}
|
||||
|
||||
function AtInputGroupController ($scope, $compile) {
|
||||
let vm = this || {};
|
||||
|
||||
let form;
|
||||
let scope;
|
||||
let state;
|
||||
let source;
|
||||
let element;
|
||||
|
||||
vm.init = (_scope_, _form_, _element_) => {
|
||||
form = _form_;
|
||||
scope = _scope_;
|
||||
element = _element_;
|
||||
state = scope.state || {};
|
||||
source = state._source;
|
||||
|
||||
$scope.$watch('state._source._value', vm.update);
|
||||
};
|
||||
|
||||
vm.isValidSource = () => {
|
||||
if (!source._value || source._value === state._value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
vm.update = () => {
|
||||
if (!vm.isValidSource()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state._group) {
|
||||
vm.clear();
|
||||
}
|
||||
|
||||
state._value = source._value;
|
||||
|
||||
let inputs = state._get(source._value);
|
||||
let group = vm.createComponentConfigs(inputs);
|
||||
|
||||
vm.insert(group);
|
||||
state._group = group;
|
||||
vm.compile(group);
|
||||
};
|
||||
|
||||
vm.createComponentConfigs = inputs => {
|
||||
let group = [];
|
||||
|
||||
inputs.forEach((input, i) => {
|
||||
input = Object.assign(input, vm.getComponentType(input));
|
||||
|
||||
group.push(Object.assign({
|
||||
_element: vm.createComponent(input, i),
|
||||
_key: 'inputs',
|
||||
_group: true,
|
||||
_groupIndex: i
|
||||
}, input));
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
vm.getComponentType = input => {
|
||||
let config = {};
|
||||
|
||||
if (input.type === 'string') {
|
||||
if (!input.multiline) {
|
||||
if (input.secret) {
|
||||
config._component = 'at-input-secret';
|
||||
} else {
|
||||
config._component = 'at-input-text';
|
||||
}
|
||||
} else {
|
||||
config._expand = true;
|
||||
|
||||
if (input.secret) {
|
||||
config._component = 'at-input-textarea-secret';
|
||||
} else {
|
||||
config._component = 'at-input-textarea';
|
||||
}
|
||||
}
|
||||
|
||||
if (input.format === 'ssh_private_key') {
|
||||
config._format = 'ssh-key';
|
||||
}
|
||||
} else if (input.type === 'number') {
|
||||
config._component = 'at-input-number';
|
||||
} else if (input.type === 'boolean') {
|
||||
config._component = 'at-input-checkbox';
|
||||
} else if (input.choices) {
|
||||
config._component = 'at-input-select';
|
||||
config._format = 'array';
|
||||
config._data = input.choices;
|
||||
config._exp = 'index as choice for (index, choice) in state._data';
|
||||
} else {
|
||||
throw new Error('Unsupported input type: ' + input.type)
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
vm.insert = group => {
|
||||
let container = document.createElement('div');
|
||||
let col = 1;
|
||||
let colPerRow = 12 / scope.col;
|
||||
let isDivided = true;
|
||||
|
||||
group.forEach((input, i) => {
|
||||
if (input._expand && !isDivided) {
|
||||
container.appendChild(vm.createDivider()[0]);
|
||||
}
|
||||
|
||||
container.appendChild(input._element[0]);
|
||||
|
||||
if ((input._expand || col % colPerRow === 0) && i !== group.length -1) {
|
||||
container.appendChild(vm.createDivider()[0]);
|
||||
isDivided = true;
|
||||
col = 0;
|
||||
} else {
|
||||
isDivided = false;
|
||||
}
|
||||
|
||||
col++;
|
||||
});
|
||||
|
||||
element.appendChild(container);
|
||||
};
|
||||
|
||||
vm.createComponent = (input, index) => {
|
||||
let tabindex = Number(scope.tab) + index;
|
||||
let col = input._expand ? 12 : scope.col;
|
||||
|
||||
return angular.element(
|
||||
`<${input._component} col="${col}" tab="${tabindex}"
|
||||
state="${state._reference}._group[${index}]">
|
||||
</${input._component}>`
|
||||
);
|
||||
};
|
||||
|
||||
vm.createDivider = () => {
|
||||
return angular.element('<at-divider></at-divider>');
|
||||
};
|
||||
|
||||
vm.compile = group => {
|
||||
group.forEach(component => $compile(component._element[0])(scope.$parent));
|
||||
};
|
||||
|
||||
vm.clear = () => {
|
||||
form.deregisterInputGroup(state._group);
|
||||
element.innerHTML = '';
|
||||
};
|
||||
}
|
||||
|
||||
AtInputGroupController.$inject = ['$scope', '$compile'];
|
||||
|
||||
function atInputGroup (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
require: ['atInputGroup', '^^atForm'],
|
||||
templateUrl: pathService.getPartialPath('components/input/group'),
|
||||
controller: AtInputGroupController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputGroupLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputGroup.$inject = ['PathService'];
|
||||
|
||||
export default atInputGroup;
|
||||
13
awx/ui/client/lib/components/input/group.partial.html
Normal file
13
awx/ui/client/lib/components/input/group.partial.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div ng-show="state._value" class="col-sm-12 at-InputGroup">
|
||||
<div class="at-InputGroup-border"></div>
|
||||
<div class="at-InputGroup-inset">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h4 class="at-InputGroup-title">
|
||||
<ng-transclude></ng-transclude>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="at-InputGroup-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
11
awx/ui/client/lib/components/input/label.directive.js
Normal file
11
awx/ui/client/lib/components/input/label.directive.js
Normal file
@ -0,0 +1,11 @@
|
||||
function atInputLabel (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: pathService.getPartialPath('components/input/label')
|
||||
};
|
||||
}
|
||||
|
||||
atInputLabel.$inject = ['PathService'];
|
||||
|
||||
export default atInputLabel;
|
||||
14
awx/ui/client/lib/components/input/label.partial.html
Normal file
14
awx/ui/client/lib/components/input/label.partial.html
Normal file
@ -0,0 +1,14 @@
|
||||
<label class="at-InputLabel">
|
||||
<span ng-if="state.required" class="at-InputLabel-required">*</span>
|
||||
<span class="at-InputLabel-name">{{::state.label}}</span>
|
||||
<at-popover state="state"></at-popover>
|
||||
<span ng-if="state._displayHint" class="at-InputLabel-hint">{{::state._hint}}</span>
|
||||
<div ng-if="state._displayPromptOnLaunch" class="at-InputLabel-checkbox pull-right">
|
||||
<label class="at-InputLabel-checkboxLabel">
|
||||
<input type="checkbox"
|
||||
ng-model="state._promptOnLaunch"
|
||||
ng-change="vm.togglePromptOnLaunch()" />
|
||||
<p>Prompt on launch</p>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
70
awx/ui/client/lib/components/input/lookup.directive.js
Normal file
70
awx/ui/client/lib/components/input/lookup.directive.js
Normal file
@ -0,0 +1,70 @@
|
||||
function atInputLookupLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputLookupController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
vm.lookup = {};
|
||||
|
||||
vm.init = (scope, element, form) => {
|
||||
baseInputController.call(vm, 'input', scope, element, form);
|
||||
|
||||
vm.lookup.modal = {
|
||||
title: 'Select Organization',
|
||||
buttons: [
|
||||
{
|
||||
type: 'cancel'
|
||||
},
|
||||
{
|
||||
type: 'select'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
vm.lookup.search = {
|
||||
placeholder: 'test'
|
||||
};
|
||||
|
||||
vm.lookup.table = {
|
||||
|
||||
};
|
||||
|
||||
vm.check();
|
||||
};
|
||||
|
||||
vm.search = () => {
|
||||
vm.modal.show('test');
|
||||
};
|
||||
}
|
||||
|
||||
AtInputLookupController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputLookup (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputLookup'],
|
||||
templateUrl: pathService.getPartialPath('components/input/lookup'),
|
||||
controller: AtInputLookupController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputLookupLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputLookup.$inject = ['PathService'];
|
||||
|
||||
export default atInputLookup;
|
||||
30
awx/ui/client/lib/components/input/lookup.partial.html
Normal file
30
awx/ui/client/lib/components/input/lookup.partial.html
Normal file
@ -0,0 +1,30 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn at-ButtonHollow--white at-Input-button"
|
||||
ng-disabled="state._disabled || form.disabled"
|
||||
ng-click="vm.search()">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</span>
|
||||
<input type="text"
|
||||
class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-model="state._value"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
</div>
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
|
||||
<at-modal state="vm.lookup">
|
||||
<at-search></at-search>
|
||||
<at-table></at-table>
|
||||
</at-modal>
|
||||
</div>
|
||||
11
awx/ui/client/lib/components/input/message.directive.js
Normal file
11
awx/ui/client/lib/components/input/message.directive.js
Normal file
@ -0,0 +1,11 @@
|
||||
function atInputMessage (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: pathService.getPartialPath('components/input/message'),
|
||||
};
|
||||
}
|
||||
|
||||
atInputMessage.$inject = ['PathService'];
|
||||
|
||||
export default atInputMessage;
|
||||
4
awx/ui/client/lib/components/input/message.partial.html
Normal file
4
awx/ui/client/lib/components/input/message.partial.html
Normal file
@ -0,0 +1,4 @@
|
||||
<p ng-if="state._rejected && !state._isValid" class="at-InputMessage--rejected">
|
||||
{{ state._message }}
|
||||
</p>
|
||||
|
||||
54
awx/ui/client/lib/components/input/number.directive.js
Normal file
54
awx/ui/client/lib/components/input/number.directive.js
Normal file
@ -0,0 +1,54 @@
|
||||
const DEFAULT_STEP = '1';
|
||||
const DEFAULT_MIN = '0';
|
||||
const DEFAULT_MAX = '1000000000';
|
||||
const DEFAULT_PLACEHOLDER = '';
|
||||
|
||||
function atInputNumberLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputNumberController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
vm.init = (scope, element, form) => {
|
||||
baseInputController.call(vm, 'input', scope, element, form);
|
||||
|
||||
scope.state._step = scope.state._step || DEFAULT_STEP;
|
||||
scope.state._min = scope.state._min || DEFAULT_MIN;
|
||||
scope.state._max = scope.state._max || DEFAULT_MAX;
|
||||
scope.state._placeholder = scope.state._placeholder || DEFAULT_PLACEHOLDER;
|
||||
|
||||
vm.check();
|
||||
};
|
||||
}
|
||||
|
||||
AtInputNumberController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputNumber (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputNumber'],
|
||||
templateUrl: pathService.getPartialPath('components/input/number'),
|
||||
controller: AtInputNumberController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputNumberLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputNumber.$inject = ['PathService'];
|
||||
|
||||
export default atInputNumber;
|
||||
19
awx/ui/client/lib/components/input/number.partial.html
Normal file
19
awx/ui/client/lib/components/input/number.partial.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<input type="number"
|
||||
class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': state.rejected }"
|
||||
ng-model="state._value"
|
||||
ng-attr-min="state._min"
|
||||
ng-attr-max="state._max"
|
||||
ng-attr-step="state._step"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
69
awx/ui/client/lib/components/input/secret.directive.js
Normal file
69
awx/ui/client/lib/components/input/secret.directive.js
Normal file
@ -0,0 +1,69 @@
|
||||
function atInputSecretLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputSecretController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
let scope;
|
||||
|
||||
vm.init = (_scope_, element, form) => {
|
||||
baseInputController.call(vm, 'input', _scope_, element, form);
|
||||
|
||||
scope = _scope_;
|
||||
|
||||
if (!scope.state._value || scope.state._promptOnLaunch) {
|
||||
scope.state._buttonText = 'SHOW';
|
||||
scope.type = 'password';
|
||||
|
||||
vm.toggle = vm.toggleShowHide;
|
||||
} else {
|
||||
scope.state._buttonText = 'REPLACE';
|
||||
scope.state._placeholder = 'ENCRYPTED';
|
||||
vm.toggle = vm.toggleRevertReplace;
|
||||
}
|
||||
|
||||
vm.check();
|
||||
};
|
||||
|
||||
vm.toggleShowHide = () => {
|
||||
if (scope.type === 'password') {
|
||||
scope.type = 'text';
|
||||
scope.state._buttonText = 'HIDE';
|
||||
} else {
|
||||
scope.type = 'password';
|
||||
scope.state._buttonText = 'SHOW';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AtInputSecretController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputSecret (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputSecret'],
|
||||
templateUrl: pathService.getPartialPath('components/input/secret'),
|
||||
controller: AtInputSecretController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputSecretLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputSecret.$inject = ['PathService'];
|
||||
|
||||
export default atInputSecret;
|
||||
26
awx/ui/client/lib/components/input/secret.partial.html
Normal file
26
awx/ui/client/lib/components/input/secret.partial.html
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn at-InputGroup-button">
|
||||
<button class="btn at-ButtonHollow--white at-Input-button"
|
||||
ng-disabled="!state._enableToggle && (state._disabled || form.disabled)"
|
||||
ng-click="vm.toggle()">
|
||||
{{ state._buttonText }}
|
||||
</button>
|
||||
</span>
|
||||
<input type="{{ type }}"
|
||||
class="form-control at-Input"
|
||||
ng-model="state[state._activeModel]"
|
||||
ng-class="{ 'at-Input--rejected': state._rejected }"
|
||||
ng-attr-maxlength="{{ state.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
</div>
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
97
awx/ui/client/lib/components/input/select.directive.js
Normal file
97
awx/ui/client/lib/components/input/select.directive.js
Normal file
@ -0,0 +1,97 @@
|
||||
const DEFAULT_EMPTY_PLACEHOLDER = 'NO OPTIONS AVAILABLE';
|
||||
|
||||
function atInputSelectLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
elements.select.focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputSelectController (baseInputController, eventService) {
|
||||
let vm = this || {};
|
||||
|
||||
let scope;
|
||||
let element;
|
||||
let input;
|
||||
let select;
|
||||
|
||||
vm.init = (_scope_, _element_, form) => {
|
||||
baseInputController.call(vm, 'input', _scope_, _element_, form);
|
||||
|
||||
scope = _scope_;
|
||||
element = _element_;
|
||||
input = element.find('input')[0];
|
||||
select = element.find('select')[0];
|
||||
|
||||
if (!scope.state._data || scope.state._data.length === 0) {
|
||||
scope.state._disabled = true;
|
||||
scope.state._placeholder = DEFAULT_EMPTY_PLACEHOLDER;
|
||||
}
|
||||
|
||||
vm.setListeners();
|
||||
vm.check();
|
||||
|
||||
if (scope.state._value) {
|
||||
vm.updateDisplayModel();
|
||||
}
|
||||
};
|
||||
|
||||
vm.setListeners = () => {
|
||||
let listeners = eventService.addListeners([
|
||||
[input, 'focus', () => select.focus],
|
||||
[select, 'mousedown', () => scope.$apply(() => scope.open = !scope.open)],
|
||||
[select, 'focus', () => input.classList.add('at-Input--focus')],
|
||||
[select, 'change', () => scope.$apply(() => {
|
||||
scope.open = false;
|
||||
vm.updateDisplayModel();
|
||||
vm.check();
|
||||
})],
|
||||
[select, 'blur', () => {
|
||||
input.classList.remove('at-Input--focus');
|
||||
scope.open = scope.open && false;
|
||||
}]
|
||||
]);
|
||||
|
||||
scope.$on('$destroy', () => eventService.remove(listeners));
|
||||
};
|
||||
|
||||
vm.updateDisplayModel = () => {
|
||||
if (scope.state._format === 'array') {
|
||||
scope.displayModel = scope.state._data[scope.state._value];
|
||||
} else if (scope.state._format === 'objects') {
|
||||
scope.displayModel = scope.state._value[scope.state._display];
|
||||
} else if (scope.state._format === 'grouped-object') {
|
||||
scope.displayModel = scope.state._value[scope.state._display];
|
||||
} else {
|
||||
throw new Error('Unsupported display model type');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AtInputSelectController.$inject = ['BaseInputController', 'EventService'];
|
||||
|
||||
function atInputSelect (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^at-form', 'atInputSelect'],
|
||||
templateUrl: pathService.getPartialPath('components/input/select'),
|
||||
controller: AtInputSelectController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputSelectLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputSelect.$inject = ['PathService'];
|
||||
|
||||
export default atInputSelect;
|
||||
26
awx/ui/client/lib/components/input/select.partial.html
Normal file
26
awx/ui/client/lib/components/input/select.partial.html
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<div class="at-InputSelect">
|
||||
<input type="text"
|
||||
class="form-control at-Input at-InputSelect-input"
|
||||
placeholder="{{state._placeholder || undefined }}"
|
||||
ng-class="{ 'at-Input--rejected': state._rejected }"
|
||||
ng-model="displayModel"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
|
||||
<select class="form-control at-InputSelect-select"
|
||||
ng-model="state._value"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-disabled="state._disabled || form.disabled"
|
||||
ng-options="{{ state._exp }}">
|
||||
<option style="display:none"></option>
|
||||
</select>
|
||||
|
||||
<i class="fa" ng-class="{ 'fa-chevron-down': !open, 'fa-chevron-up': open }"></i>
|
||||
</div>
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
44
awx/ui/client/lib/components/input/text.directive.js
Normal file
44
awx/ui/client/lib/components/input/text.directive.js
Normal file
@ -0,0 +1,44 @@
|
||||
function atInputTextLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputTextController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
vm.init = (scope, element, form) => {
|
||||
baseInputController.call(vm, 'input', scope, element, form);
|
||||
|
||||
vm.check();
|
||||
};
|
||||
}
|
||||
|
||||
AtInputTextController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputText (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputText'],
|
||||
templateUrl: pathService.getPartialPath('components/input/text'),
|
||||
controller: AtInputTextController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputTextLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputText.$inject = ['PathService'];
|
||||
|
||||
export default atInputText;
|
||||
16
awx/ui/client/lib/components/input/text.partial.html
Normal file
16
awx/ui/client/lib/components/input/text.partial.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<input type="text" class="form-control at-Input"
|
||||
ng-class="{ 'at-Input--rejected': state._rejected }"
|
||||
ng-model="state._value"
|
||||
ng-attr-maxlength="{{ state.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
117
awx/ui/client/lib/components/input/textarea-secret.directive.js
Normal file
117
awx/ui/client/lib/components/input/textarea-secret.directive.js
Normal file
@ -0,0 +1,117 @@
|
||||
const DEFAULT_HINT = 'HINT: Drag and drop an SSH private key file on the field below.';
|
||||
|
||||
function atInputTextareaSecretLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('textarea')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputTextareaSecretController (baseInputController, eventService) {
|
||||
let vm = this || {};
|
||||
|
||||
let scope;
|
||||
let textarea;
|
||||
let container;
|
||||
let input;
|
||||
|
||||
vm.init = (_scope_, element, form) => {
|
||||
baseInputController.call(vm, 'input', _scope_, element, form);
|
||||
|
||||
scope = _scope_;
|
||||
textarea = element.find('textarea')[0];
|
||||
container = element[0];
|
||||
|
||||
if (scope.state.format === 'ssh_private_key') {
|
||||
scope.ssh = true;
|
||||
scope.state._hint = scope.state._hint || DEFAULT_HINT;
|
||||
input = element.find('input')[0];
|
||||
}
|
||||
|
||||
if (scope.state._value) {
|
||||
scope.state._buttonText = 'REPLACE';
|
||||
scope.state._placeholder = 'ENCRYPTED';
|
||||
} else {
|
||||
if (scope.state.format === 'ssh_private_key') {
|
||||
vm.listeners = vm.setFileListeners(textarea, input);
|
||||
scope.state._displayHint = true;
|
||||
}
|
||||
}
|
||||
|
||||
vm.check();
|
||||
};
|
||||
|
||||
vm.toggle = () => {
|
||||
vm.toggleRevertReplace();
|
||||
|
||||
if (scope.state._isBeingReplaced) {
|
||||
scope.state._placeholder = '';
|
||||
scope.state._displayHint = true;
|
||||
vm.listeners = vm.setFileListeners(textarea, input);
|
||||
} else {
|
||||
scope.state._displayHint = false;
|
||||
scope.state._placeholder = 'ENCRYPTED';
|
||||
eventService.remove(vm.listeners);
|
||||
}
|
||||
};
|
||||
|
||||
vm.setFileListeners = (textarea, input) => {
|
||||
return eventService.addListeners([
|
||||
[textarea, 'dragenter', event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
scope.$apply(() => scope.drag = true);
|
||||
}],
|
||||
|
||||
[input, 'dragleave', event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
scope.$apply(() => scope.drag = false);
|
||||
}],
|
||||
|
||||
[input, 'change', event => {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = () => vm.readFile(reader, event);
|
||||
reader.readAsText(input.files[0]);
|
||||
}]
|
||||
]);
|
||||
};
|
||||
|
||||
vm.readFile = (reader, event) => {
|
||||
scope.$apply(() => {
|
||||
scope.state._value = reader.result;
|
||||
vm.check();
|
||||
scope.drag = false
|
||||
input.value = '';
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
AtInputTextareaSecretController.$inject = ['BaseInputController', 'EventService'];
|
||||
|
||||
function atInputTextareaSecret (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputTextareaSecret'],
|
||||
templateUrl: pathService.getPartialPath('components/input/textarea-secret'),
|
||||
controller: AtInputTextareaSecretController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputTextareaSecretLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputTextareaSecret.$inject = ['PathService'];
|
||||
|
||||
export default atInputTextareaSecret;
|
||||
@ -0,0 +1,32 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<div ng-class="{ 'input-group': state._edit }">
|
||||
<span ng-if="state._edit" class="input-group-btn at-InputGroup-button">
|
||||
<button class="btn at-ButtonHollow--white at-Input-button"
|
||||
ng-disabled="!state._enableToggle && (state._disabled || form.disabled)"
|
||||
ng-click="vm.toggle()">
|
||||
{{ state._buttonText }}
|
||||
</button>
|
||||
</span>
|
||||
<input ng-show="ssh"
|
||||
class="at-InputFile--hidden"
|
||||
ng-class="{'at-InputFile--drag': drag }"
|
||||
type="file"
|
||||
name="files" />
|
||||
<textarea class="form-control at-Input at-InputTextarea"
|
||||
ng-model="state[state._activeModel]"
|
||||
ng-class="{ 'at-Input--rejected': state._rejected }"
|
||||
ng-attr-rows="{{::state._rows || 6 }}"
|
||||
ng-attr-maxlength="{{ state.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" />
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
44
awx/ui/client/lib/components/input/textarea.directive.js
Normal file
44
awx/ui/client/lib/components/input/textarea.directive.js
Normal file
@ -0,0 +1,44 @@
|
||||
function atInputTextareaLink (scope, element, attrs, controllers) {
|
||||
let formController = controllers[0];
|
||||
let inputController = controllers[1];
|
||||
|
||||
if (scope.tab === '1') {
|
||||
element.find('input')[0].focus();
|
||||
}
|
||||
|
||||
inputController.init(scope, element, formController);
|
||||
}
|
||||
|
||||
function AtInputTextareaController (baseInputController) {
|
||||
let vm = this || {};
|
||||
|
||||
vm.init = (scope, element, form) => {
|
||||
baseInputController.call(vm, 'input', scope, element, form);
|
||||
|
||||
vm.check();
|
||||
};
|
||||
}
|
||||
|
||||
AtInputTextareaController.$inject = ['BaseInputController'];
|
||||
|
||||
function atInputTextarea (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
require: ['^^atForm', 'atInputTextarea'],
|
||||
templateUrl: pathService.getPartialPath('components/input/textarea'),
|
||||
controller: AtInputTextareaController,
|
||||
controllerAs: 'vm',
|
||||
link: atInputTextareaLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
col: '@',
|
||||
tab: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atInputTextarea.$inject = ['PathService'];
|
||||
|
||||
export default atInputTextarea;
|
||||
17
awx/ui/client/lib/components/input/textarea.partial.html
Normal file
17
awx/ui/client/lib/components/input/textarea.partial.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="col-sm-{{::col}} at-InputContainer">
|
||||
<div class="form-group at-u-flat">
|
||||
<at-input-label></at-input-label>
|
||||
|
||||
<textarea class="form-control at-Input at-InputTextarea"
|
||||
ng-model="state._value"
|
||||
ng-class="{ 'at-Input--rejected': state._rejected }"
|
||||
ng-attr-rows="{{::state._rows || 6 }}"
|
||||
ng-attr-maxlength="{{ state.max_length || undefined }}"
|
||||
ng-attr-tabindex="{{ tab || undefined }}"
|
||||
ng-attr-placeholder="{{::state._placeholder || undefined }}"
|
||||
ng-change="vm.check()"
|
||||
ng-disabled="state._disabled || form.disabled" /></textarea>
|
||||
|
||||
<at-input-message></at-input-message>
|
||||
</div>
|
||||
</div>
|
||||
10
awx/ui/client/lib/components/modal/_index.less
Normal file
10
awx/ui/client/lib/components/modal/_index.less
Normal file
@ -0,0 +1,10 @@
|
||||
.at-Modal-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.at-mixin-Heading(@at-font-size-3x);
|
||||
}
|
||||
|
||||
.at-Modal-body {
|
||||
font-size: @at-font-size;
|
||||
}
|
||||
63
awx/ui/client/lib/components/modal/modal.directive.js
Normal file
63
awx/ui/client/lib/components/modal/modal.directive.js
Normal file
@ -0,0 +1,63 @@
|
||||
const DEFAULT_ANIMATION_DURATION = 150;
|
||||
|
||||
function atModalLink (scope, el, attr, controllers) {
|
||||
let modalController = controllers[0];
|
||||
let container = el[0];
|
||||
|
||||
modalController.init(scope, container);
|
||||
}
|
||||
|
||||
function AtModalController () {
|
||||
let vm = this;
|
||||
|
||||
let scope;
|
||||
let container;
|
||||
|
||||
vm.init = (_scope_, _container_) => {
|
||||
scope = _scope_;
|
||||
container = _container_;
|
||||
|
||||
scope.state.show = vm.show;
|
||||
scope.state.hide = vm.hide;
|
||||
};
|
||||
|
||||
vm.show = (title, message) => {
|
||||
scope.title = title;
|
||||
scope.message = message;
|
||||
|
||||
container.style.display = 'block';
|
||||
container.style.opacity = 1;
|
||||
};
|
||||
|
||||
vm.hide = () => {
|
||||
container.style.opacity = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
container.style.display = 'none';
|
||||
scope.message = '';
|
||||
scope.title = '';
|
||||
}, DEFAULT_ANIMATION_DURATION);
|
||||
};
|
||||
}
|
||||
|
||||
function atModal (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
require: ['atModal'],
|
||||
templateUrl: pathService.getPartialPath('components/modal/modal'),
|
||||
controller: AtModalController,
|
||||
controllerAs: 'vm',
|
||||
link: atModalLink,
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atModal.$inject = [
|
||||
'PathService'
|
||||
];
|
||||
|
||||
export default atModal;
|
||||
21
awx/ui/client/lib/components/modal/modal.partial.html
Normal file
21
awx/ui/client/lib/components/modal/modal.partial.html
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="modal at-Modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="vm.hide()">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<h4 class="modal-title at-Modal-title">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="modal-body at-Modal-body">
|
||||
<p ng-show="message">{{ message }}</p>
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn at-ButtonHollow--white" ng-click="vm.hide()">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
25
awx/ui/client/lib/components/panel/_index.less
Normal file
25
awx/ui/client/lib/components/panel/_index.less
Normal file
@ -0,0 +1,25 @@
|
||||
.at-Panel {
|
||||
margin: @at-space-6x 0 0 0;
|
||||
padding: @at-space-6x;
|
||||
border-color: @at-gray-dark;
|
||||
}
|
||||
|
||||
.at-Panel-heading {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-Panel-dismiss {
|
||||
.at-mixin-ButtonIcon();
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.at-Panel-body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-Panel-headingTitle {
|
||||
.at-mixin-Heading(@at-font-size-3x);
|
||||
text-transform: none;
|
||||
}
|
||||
15
awx/ui/client/lib/components/panel/body.directive.js
Normal file
15
awx/ui/client/lib/components/panel/body.directive.js
Normal file
@ -0,0 +1,15 @@
|
||||
function atPanelBody (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
templateUrl: pathService.getPartialPath('components/panel/body'),
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atPanelBody.$inject = ['PathService'];
|
||||
|
||||
export default atPanelBody;
|
||||
3
awx/ui/client/lib/components/panel/body.partial.html
Normal file
3
awx/ui/client/lib/components/panel/body.partial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="panel-body at-Panel-body">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
18
awx/ui/client/lib/components/panel/heading.directive.js
Normal file
18
awx/ui/client/lib/components/panel/heading.directive.js
Normal file
@ -0,0 +1,18 @@
|
||||
function link (scope, el, attrs, panel) {
|
||||
panel.use(scope);
|
||||
}
|
||||
|
||||
function atPanelHeading (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: '^^atPanel',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
templateUrl: pathService.getPartialPath('components/panel/heading'),
|
||||
link
|
||||
};
|
||||
}
|
||||
|
||||
atPanelHeading.$inject = ['PathService'];
|
||||
|
||||
export default atPanelHeading;
|
||||
12
awx/ui/client/lib/components/panel/heading.partial.html
Normal file
12
awx/ui/client/lib/components/panel/heading.partial.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-10">
|
||||
<h3 class="at-Panel-headingTitle">
|
||||
<ng-transclude></ng-transclude>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<div class="at-Panel-dismiss">
|
||||
<i class="fa fa-times-circle fa-lg" ng-click="dismiss()"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
48
awx/ui/client/lib/components/panel/panel.directive.js
Normal file
48
awx/ui/client/lib/components/panel/panel.directive.js
Normal file
@ -0,0 +1,48 @@
|
||||
function atPanelLink (scope, el, attrs, controllers) {
|
||||
let panelController = controllers[0];
|
||||
|
||||
panelController.init(scope, el);
|
||||
}
|
||||
|
||||
function AtPanelController ($state) {
|
||||
let vm = this;
|
||||
|
||||
let scope;
|
||||
let el;
|
||||
|
||||
vm.init = (_scope_, _el_) => {
|
||||
scope = _scope_;
|
||||
el = _el_;
|
||||
};
|
||||
|
||||
vm.dismiss = () => {
|
||||
$state.go('^');
|
||||
};
|
||||
|
||||
vm.use = child => {
|
||||
child.dismiss = vm.dismiss;
|
||||
};
|
||||
}
|
||||
|
||||
AtPanelController.$inject = ['$state'];
|
||||
|
||||
function atPanel (pathService, _$animate_) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
require: ['atPanel'],
|
||||
transclude: true,
|
||||
templateUrl: pathService.getPartialPath('components/panel/panel'),
|
||||
controller: AtPanelController,
|
||||
controllerAs: 'vm',
|
||||
link: atPanelLink,
|
||||
scope: {
|
||||
state: '=',
|
||||
animate: '@'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atPanel.$inject = ['PathService'];
|
||||
|
||||
export default atPanel;
|
||||
3
awx/ui/client/lib/components/panel/panel.partial.html
Normal file
3
awx/ui/client/lib/components/panel/panel.partial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="panel panel-default at-Panel">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
51
awx/ui/client/lib/components/popover/_index.less
Normal file
51
awx/ui/client/lib/components/popover/_index.less
Normal file
@ -0,0 +1,51 @@
|
||||
.at-Popover {
|
||||
padding: 0 0 0 @at-space-3x;
|
||||
}
|
||||
|
||||
.at-Popover--inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.at-Popover-icon {
|
||||
.at-mixin-ButtonIcon();
|
||||
font-size: @at-font-size-4x;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.at-Popover-container {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
color: @at-white;
|
||||
background-color: @at-gray-dark-4x;
|
||||
max-width: @at-popover-width;
|
||||
padding: @at-space-4x;
|
||||
height: auto;
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
margin: 0 0 0 @at-space-6x;
|
||||
border-radius: @at-border-radius;
|
||||
box-shadow: 0 5px 10px rgba(0,0,0, 0.2);
|
||||
transition: opacity .15s linear;
|
||||
font-weight: @at-font-weight
|
||||
}
|
||||
|
||||
.at-Popover-arrow {
|
||||
color: @at-gray-dark-4x;
|
||||
position: fixed;
|
||||
z-index: 1999;
|
||||
padding: 0;
|
||||
margin: @at-space-4x 0 0 @at-space;
|
||||
}
|
||||
|
||||
.at-Popover-title {
|
||||
.at-mixin-Heading(@at-font-size);
|
||||
color: @at-white;
|
||||
margin-bottom: @at-space-4x;
|
||||
}
|
||||
|
||||
.at-Popover-text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: @at-font-size;
|
||||
}
|
||||
118
awx/ui/client/lib/components/popover/popover.directive.js
Normal file
118
awx/ui/client/lib/components/popover/popover.directive.js
Normal file
@ -0,0 +1,118 @@
|
||||
function atPopoverLink (scope, el, attr, controllers) {
|
||||
let popoverController = controllers[0];
|
||||
let container = el[0];
|
||||
let popover = container.getElementsByClassName('at-Popover-container')[0];
|
||||
let icon = container.getElementsByTagName('i')[0];
|
||||
|
||||
popoverController.init(scope, container, icon, popover);
|
||||
}
|
||||
|
||||
function AtPopoverController () {
|
||||
let vm = this;
|
||||
|
||||
let container;
|
||||
let icon;
|
||||
let popover;
|
||||
|
||||
vm.init = (scope, _container_, _icon_, _popover_) => {
|
||||
icon = _icon_;
|
||||
popover = _popover_;
|
||||
scope.inline = true;
|
||||
|
||||
icon.addEventListener('click', vm.createDisplayListener());
|
||||
};
|
||||
|
||||
vm.createDismissListener = (createEvent) => {
|
||||
return event => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (vm.isClickWithinPopover(event, popover)) {
|
||||
return;
|
||||
}
|
||||
|
||||
vm.open = false;
|
||||
|
||||
popover.style.visibility = 'hidden';
|
||||
popover.style.opacity = 0;
|
||||
|
||||
window.removeEventListener('click', vm.dismissListener);
|
||||
window.removeEventListener('resize', vm.dismissListener);
|
||||
};
|
||||
};
|
||||
|
||||
vm.isClickWithinPopover = (event, popover) => {
|
||||
let box = popover.getBoundingClientRect();
|
||||
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
|
||||
if ((x <= box.right && x >= box.left) && (y >= box.top && y <= box.bottom)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
vm.createDisplayListener = () => {
|
||||
return event => {
|
||||
if (vm.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
vm.open = true;
|
||||
|
||||
let arrow = popover.getElementsByClassName('at-Popover-arrow')[0];
|
||||
|
||||
let iPos = icon.getBoundingClientRect();
|
||||
let pPos = popover.getBoundingClientRect();
|
||||
|
||||
let wHeight = window.clientHeight;
|
||||
let pHeight = pPos.height;
|
||||
|
||||
let cx = Math.floor(iPos.left + (iPos.width / 2));
|
||||
let cy = Math.floor(iPos.top + (iPos.height / 2));
|
||||
|
||||
arrow.style.top = (iPos.top - iPos.height) + 'px';
|
||||
arrow.style.left = iPos.right + 'px';
|
||||
|
||||
if (cy < (pHeight / 2)) {
|
||||
popover.style.top = '10px';
|
||||
} else {
|
||||
popover.style.top = (cy - pHeight / 2) + 'px';
|
||||
}
|
||||
|
||||
popover.style.left = cx + 'px';
|
||||
popover.style.visibility = 'visible';
|
||||
popover.style.opacity = 1;
|
||||
|
||||
vm.dismissListener = vm.createDismissListener(event);
|
||||
|
||||
window.addEventListener('click', vm.dismissListener);
|
||||
window.addEventListener('resize', vm.dismissListener);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function atPopover (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
require: ['atPopover'],
|
||||
templateUrl: pathService.getPartialPath('components/popover/popover'),
|
||||
controller: AtPopoverController,
|
||||
controllerAs: 'vm',
|
||||
link: atPopoverLink,
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atPopover.$inject = [
|
||||
'PathService'
|
||||
];
|
||||
|
||||
export default atPopover;
|
||||
16
awx/ui/client/lib/components/popover/popover.partial.html
Normal file
16
awx/ui/client/lib/components/popover/popover.partial.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div ng-show="state.help_text"
|
||||
class="at-Popover"
|
||||
ng-class="{ 'at-Popover--inline': inline }">
|
||||
<span class="at-Popover-icon">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
<div class="at-Popover-container">
|
||||
<div class="at-Popover-arrow">
|
||||
<i class="fa fa-caret-left fa-2x"></i>
|
||||
</div>
|
||||
<div class="at-Popover-content">
|
||||
<h4 class="at-Popover-title">{{::state.label}}</h4>
|
||||
<p class="at-Popover-text">{{::state.help_text}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
awx/ui/client/lib/components/tabs/_index.less
Normal file
27
awx/ui/client/lib/components/tabs/_index.less
Normal file
@ -0,0 +1,27 @@
|
||||
.at-TabGroup {
|
||||
margin-top: @at-space-6x;
|
||||
}
|
||||
|
||||
.at-Tab {
|
||||
margin: 0 @at-space-5x 0 0;
|
||||
font-size: @at-font-size;
|
||||
}
|
||||
|
||||
.at-Tab--active {
|
||||
&, &:hover, &:active, &:focus {
|
||||
color: @at-white;
|
||||
background-color: @at-gray-dark-3x;
|
||||
border-color: @at-gray-dark-3x;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.at-Tab--disabled {
|
||||
&, &:hover, &:active, &:focus {
|
||||
background-color: @at-white;
|
||||
color: @at-gray-dark-2x;
|
||||
border-color: @at-gray-dark-2x;
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
55
awx/ui/client/lib/components/tabs/group.directive.js
Normal file
55
awx/ui/client/lib/components/tabs/group.directive.js
Normal file
@ -0,0 +1,55 @@
|
||||
function atTabGroupLink (scope, el, attrs, controllers) {
|
||||
let groupController = controllers[0];
|
||||
|
||||
groupController.init(scope, el);
|
||||
}
|
||||
|
||||
function AtTabGroupController ($state) {
|
||||
let vm = this;
|
||||
|
||||
vm.tabs = [];
|
||||
|
||||
let scope;
|
||||
let el;
|
||||
|
||||
vm.init = (_scope_, _el_) => {
|
||||
scope = _scope_;
|
||||
el = _el_;
|
||||
};
|
||||
|
||||
vm.register = tab => {
|
||||
|
||||
tab.active = true;
|
||||
/*
|
||||
* if (vm.tabs.length === 0) {
|
||||
* tab.active = true;
|
||||
* } else {
|
||||
* tab.disabled = true;
|
||||
* }
|
||||
*
|
||||
*/
|
||||
vm.tabs.push(tab);
|
||||
};
|
||||
}
|
||||
|
||||
AtTabGroupController.$inject = ['$state'];
|
||||
|
||||
function atTabGroup (pathService, _$animate_) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
require: ['atTabGroup'],
|
||||
transclude: true,
|
||||
templateUrl: pathService.getPartialPath('components/tabs/group'),
|
||||
controller: AtTabGroupController,
|
||||
controllerAs: 'vm',
|
||||
link: atTabGroupLink,
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atTabGroup.$inject = ['PathService'];
|
||||
|
||||
export default atTabGroup;
|
||||
3
awx/ui/client/lib/components/tabs/group.partial.html
Normal file
3
awx/ui/client/lib/components/tabs/group.partial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="at-TabGroup">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
52
awx/ui/client/lib/components/tabs/tab.directive.js
Normal file
52
awx/ui/client/lib/components/tabs/tab.directive.js
Normal file
@ -0,0 +1,52 @@
|
||||
function atTabLink (scope, el, attrs, controllers) {
|
||||
let groupController = controllers[0];
|
||||
let tabController = controllers[1];
|
||||
|
||||
tabController.init(scope, el, groupController);
|
||||
}
|
||||
|
||||
function AtTabController ($state) {
|
||||
let vm = this;
|
||||
|
||||
let scope;
|
||||
let el;
|
||||
let group;
|
||||
|
||||
vm.init = (_scope_, _el_, _group_) => {
|
||||
scope = _scope_;
|
||||
el = _el_;
|
||||
group = _group_;
|
||||
|
||||
group.register(scope);
|
||||
};
|
||||
|
||||
vm.go = () => {
|
||||
if (scope.state._disabled || scope.state._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state.go(scope.state._go, scope.state._params, { reload: true });
|
||||
};
|
||||
}
|
||||
|
||||
AtTabController.$inject = ['$state'];
|
||||
|
||||
function atTab (pathService, _$animate_) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
require: ['^^atTabGroup', 'atTab'],
|
||||
templateUrl: pathService.getPartialPath('components/tabs/tab'),
|
||||
controller: AtTabController,
|
||||
controllerAs: 'vm',
|
||||
link: atTabLink,
|
||||
scope: {
|
||||
state: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
atTab.$inject = ['PathService'];
|
||||
|
||||
export default atTab;
|
||||
6
awx/ui/client/lib/components/tabs/tab.partial.html
Normal file
6
awx/ui/client/lib/components/tabs/tab.partial.html
Normal file
@ -0,0 +1,6 @@
|
||||
<button class="btn at-ButtonHollow--white at-Tab"
|
||||
ng-attr-disabled="{{ state._disabled || undefined }}"
|
||||
ng-class="{ 'at-Tab--active': state._active, 'at-Tab--disabled': state._disabled }"
|
||||
ng-click="vm.go()">
|
||||
<ng-transclude></ng-transclude>
|
||||
</button>
|
||||
5
awx/ui/client/lib/components/utility/_index.less
Normal file
5
awx/ui/client/lib/components/utility/_index.less
Normal file
@ -0,0 +1,5 @@
|
||||
.at-Divider {
|
||||
clear: both;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
12
awx/ui/client/lib/components/utility/divider.directive.js
Normal file
12
awx/ui/client/lib/components/utility/divider.directive.js
Normal file
@ -0,0 +1,12 @@
|
||||
function atPanelBody (pathService) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: pathService.getPartialPath('components/utility/divider'),
|
||||
scope: false
|
||||
};
|
||||
}
|
||||
|
||||
atPanelBody.$inject = ['PathService'];
|
||||
|
||||
export default atPanelBody;
|
||||
@ -0,0 +1 @@
|
||||
<div class="at-Divider"></div>
|
||||
162
awx/ui/client/lib/models/Base.js
Normal file
162
awx/ui/client/lib/models/Base.js
Normal file
@ -0,0 +1,162 @@
|
||||
let $http;
|
||||
let $q;
|
||||
|
||||
function request (method, resource) {
|
||||
if (Array.isArray(method) && Array.isArray(resource)) {
|
||||
let promises = method.map((value, i) => this.http[value](resource[i]));
|
||||
|
||||
return $q.all(promises);
|
||||
}
|
||||
|
||||
return this.http[method](resource);
|
||||
}
|
||||
|
||||
function httpGet (resource) {
|
||||
let req = {
|
||||
method: 'GET',
|
||||
url: this.path
|
||||
};
|
||||
|
||||
if (typeof resource === 'object') {
|
||||
this.model[this.method] = resource;
|
||||
|
||||
return $q.resolve();
|
||||
} else if (resource) {
|
||||
req.url = `${this.path}${resource}/`;
|
||||
}
|
||||
|
||||
return $http(req)
|
||||
.then(res => {
|
||||
this.model.GET = res.data;
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function httpPost (data) {
|
||||
let req = {
|
||||
method: 'POST',
|
||||
url: this.path,
|
||||
data
|
||||
};
|
||||
|
||||
return $http(req).then(res => {
|
||||
this.model.GET = res.data;
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function httpPut (changes) {
|
||||
let model = Object.assign(this.get(), changes);
|
||||
|
||||
let req = {
|
||||
method: 'PUT',
|
||||
url: `${this.path}${model.id}/`,
|
||||
data: model
|
||||
};
|
||||
|
||||
return $http(req).then(res => res);
|
||||
}
|
||||
|
||||
function httpOptions (resource) {
|
||||
let req = {
|
||||
method: 'OPTIONS',
|
||||
url: this.path
|
||||
};
|
||||
|
||||
if (resource) {
|
||||
req.url = `${this.path}${resource}/`;
|
||||
}
|
||||
|
||||
return $http(req)
|
||||
.then(res => {
|
||||
this.model.OPTIONS = res.data;
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function get (method, keys) {
|
||||
let model;
|
||||
|
||||
if (keys) {
|
||||
model = this.model[method.toUpperCase()];
|
||||
} else {
|
||||
model = this.model.GET;
|
||||
keys = method;
|
||||
}
|
||||
|
||||
if (!keys) {
|
||||
return model;
|
||||
}
|
||||
|
||||
keys = keys.split('.');
|
||||
|
||||
let value = model;
|
||||
|
||||
try {
|
||||
keys.forEach(key => {
|
||||
let bracketIndex = key.indexOf('[');
|
||||
let hasArray = bracketIndex !== -1;
|
||||
|
||||
if (!hasArray) {
|
||||
value = value[key];
|
||||
return;
|
||||
}
|
||||
|
||||
if (bracketIndex === 0) {
|
||||
value = value[Number(key.substring(1, key.length - 1))];
|
||||
return;
|
||||
}
|
||||
|
||||
let prop = key.substring(0, bracketIndex);
|
||||
let index = Number(key.substring(bracketIndex + 1, key.length - 1));
|
||||
|
||||
value = value[prop][index];
|
||||
});
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizePath (resource) {
|
||||
let version = '/api/v2/';
|
||||
|
||||
return `${version}${resource}/`;
|
||||
}
|
||||
|
||||
function getById (id) {
|
||||
let item = this.get('results').filter(result => result.id === id);
|
||||
|
||||
return item ? item[0] : undefined;
|
||||
}
|
||||
|
||||
function BaseModel (path) {
|
||||
this.model = {};
|
||||
this.get = get;
|
||||
this.normalizePath = normalizePath;
|
||||
this.getById = getById;
|
||||
this.request = request;
|
||||
this.http = {
|
||||
get: httpGet.bind(this),
|
||||
options: httpOptions.bind(this),
|
||||
post: httpPost.bind(this),
|
||||
put: httpPut.bind(this)
|
||||
};
|
||||
|
||||
this.path = this.normalizePath(path);
|
||||
};
|
||||
|
||||
function BaseModelLoader (_$http_, _$q_) {
|
||||
$http = _$http_;
|
||||
$q = _$q_;
|
||||
|
||||
return BaseModel;
|
||||
}
|
||||
|
||||
BaseModelLoader.$inject = ['$http', '$q'];
|
||||
|
||||
export default BaseModelLoader;
|
||||
59
awx/ui/client/lib/models/Credential.js
Normal file
59
awx/ui/client/lib/models/Credential.js
Normal file
@ -0,0 +1,59 @@
|
||||
const ENCRYPTED_VALUE = '$encrypted$';
|
||||
|
||||
let BaseModel;
|
||||
|
||||
function createFormSchema (method, config) {
|
||||
let schema = Object.assign({}, this.get('options', `actions.${method.toUpperCase()}`));
|
||||
|
||||
if (config && config.omit) {
|
||||
config.omit.forEach(key => {
|
||||
delete schema[key];
|
||||
});
|
||||
}
|
||||
|
||||
for (let key in schema) {
|
||||
schema[key].id = key;
|
||||
|
||||
if (method === 'put') {
|
||||
schema[key]._value = this.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function assignInputGroupValues (inputs) {
|
||||
return inputs.map(input => {
|
||||
let value = this.get(`inputs.${input.id}`);
|
||||
|
||||
input._value = value;
|
||||
input._encrypted = value === ENCRYPTED_VALUE;
|
||||
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
function clearTypeInputs () {
|
||||
delete this.model.GET.inputs;
|
||||
}
|
||||
|
||||
function CredentialModel (method, resource) {
|
||||
BaseModel.call(this, 'credentials');
|
||||
|
||||
this.createFormSchema = createFormSchema.bind(this);
|
||||
this.assignInputGroupValues = assignInputGroupValues.bind(this);
|
||||
this.clearTypeInputs = clearTypeInputs.bind(this);
|
||||
|
||||
return this.request(method, resource)
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
function CredentialModelLoader (_BaseModel_ ) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return CredentialModel;
|
||||
}
|
||||
|
||||
CredentialModelLoader.$inject = ['BaseModel'];
|
||||
|
||||
export default CredentialModelLoader;
|
||||
47
awx/ui/client/lib/models/CredentialType.js
Normal file
47
awx/ui/client/lib/models/CredentialType.js
Normal file
@ -0,0 +1,47 @@
|
||||
let BaseModel;
|
||||
|
||||
function categorizeByKind () {
|
||||
let group = {};
|
||||
|
||||
this.get('results').forEach(result => {
|
||||
group[result.kind] = group[result.kind] || [];
|
||||
group[result.kind].push(result);
|
||||
});
|
||||
|
||||
return Object.keys(group).map(category => ({
|
||||
data: group[category],
|
||||
category
|
||||
}));
|
||||
}
|
||||
|
||||
function mergeInputProperties (type) {
|
||||
return type.inputs.fields.map(field => {
|
||||
if (!type.inputs.required || type.inputs.required.indexOf(field.id) === -1) {
|
||||
field.required = false;
|
||||
} else {
|
||||
field.required = true;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
function CredentialTypeModel (method, id) {
|
||||
BaseModel.call(this, 'credential_types');
|
||||
|
||||
this.categorizeByKind = categorizeByKind.bind(this);
|
||||
this.mergeInputProperties = mergeInputProperties.bind(this);
|
||||
|
||||
return this.request(method, id)
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
function CredentialTypeModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return CredentialTypeModel;
|
||||
}
|
||||
|
||||
CredentialTypeModelLoader.$inject = ['BaseModel'];
|
||||
|
||||
export default CredentialTypeModelLoader;
|
||||
24
awx/ui/client/lib/models/Me.js
Normal file
24
awx/ui/client/lib/models/Me.js
Normal file
@ -0,0 +1,24 @@
|
||||
let BaseModel;
|
||||
|
||||
function getSelf () {
|
||||
return this.get('results[0]');
|
||||
}
|
||||
|
||||
function MeModel (method) {
|
||||
BaseModel.call(this, 'me');
|
||||
|
||||
this.getSelf = getSelf.bind(this);
|
||||
|
||||
return this.request(method)
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
function MeModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return MeModel;
|
||||
}
|
||||
|
||||
MeModelLoader.$inject = ['BaseModel'];
|
||||
|
||||
export default MeModelLoader;
|
||||
18
awx/ui/client/lib/models/Organization.js
Normal file
18
awx/ui/client/lib/models/Organization.js
Normal file
@ -0,0 +1,18 @@
|
||||
let BaseModel;
|
||||
|
||||
function OrganizationModel (method) {
|
||||
BaseModel.call(this, 'organizations');
|
||||
|
||||
return this.request(method)
|
||||
.then(() => this);
|
||||
}
|
||||
|
||||
function OrganizationModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return OrganizationModel;
|
||||
}
|
||||
|
||||
OrganizationModelLoader.$inject = ['BaseModel'];
|
||||
|
||||
export default OrganizationModelLoader;
|
||||
14
awx/ui/client/lib/models/index.js
Normal file
14
awx/ui/client/lib/models/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Base from './Base';
|
||||
import Credential from './Credential';
|
||||
import CredentialType from './CredentialType';
|
||||
import Me from './Me';
|
||||
import Organization from './Organization';
|
||||
|
||||
angular
|
||||
.module('at.lib.models', [])
|
||||
.service('BaseModel', Base)
|
||||
.service('CredentialModel', Credential)
|
||||
.service('CredentialTypeModel', CredentialType)
|
||||
.service('MeModel', Me)
|
||||
.service('OrganizationModel', Organization);
|
||||
|
||||
37
awx/ui/client/lib/services/event.service.js
Normal file
37
awx/ui/client/lib/services/event.service.js
Normal file
@ -0,0 +1,37 @@
|
||||
function EventService () {
|
||||
this.addListeners = list => {
|
||||
let listeners = [];
|
||||
|
||||
list.forEach(args => listeners.push(this.addListener(...args)));
|
||||
|
||||
return listeners;
|
||||
};
|
||||
|
||||
this.addListener = (el, name, fn) => {
|
||||
let listener = {
|
||||
fn,
|
||||
name,
|
||||
el
|
||||
};
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
name.forEach(e => listener.el.addEventListener(e, listener.fn));
|
||||
} else {
|
||||
listener.el.addEventListener(listener.name, listener.fn);
|
||||
}
|
||||
|
||||
return listener;
|
||||
};
|
||||
|
||||
this.remove = listeners => {
|
||||
listeners.forEach(listener => {
|
||||
if (Array.isArray(listener.name)) {
|
||||
listener.name.forEach(name => listener.el.removeEventListener(name, listener.fn));
|
||||
} else {
|
||||
listener.el.removeEventListener(listener.name, listener.fn);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default EventService;
|
||||
7
awx/ui/client/lib/services/index.js
Normal file
7
awx/ui/client/lib/services/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import EventService from './event.service';
|
||||
import PathService from './path.service';
|
||||
|
||||
angular
|
||||
.module('at.lib.services', [])
|
||||
.service('EventService', EventService)
|
||||
.service('PathService', PathService);
|
||||
11
awx/ui/client/lib/services/path.service.js
Normal file
11
awx/ui/client/lib/services/path.service.js
Normal file
@ -0,0 +1,11 @@
|
||||
function PathService () {
|
||||
this.getPartialPath = path => {
|
||||
return `/static/partials/${path}.partial.html`;
|
||||
};
|
||||
|
||||
this.getViewPath = path => {
|
||||
return `/static/views/${path}.view.html`;
|
||||
}
|
||||
}
|
||||
|
||||
export default PathService;
|
||||
38
awx/ui/client/lib/theme/_common.less
Normal file
38
awx/ui/client/lib/theme/_common.less
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* For styles that are used in more than one place throughout the application.
|
||||
*
|
||||
* 1. Buttons
|
||||
*
|
||||
*/
|
||||
|
||||
// 1. Buttons -------------------------------------------------------------------------------------
|
||||
|
||||
.at-Button--green {
|
||||
.at-mixin-Button();
|
||||
.at-mixin-ButtonColor('at-green', 'at-white');
|
||||
|
||||
&[disabled] {
|
||||
background: @at-gray-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.at-Button--blue {
|
||||
.at-mixin-Button();
|
||||
.at-mixin-ButtonColor('at-blue', 'at-white');
|
||||
}
|
||||
|
||||
.at-Button--red {
|
||||
.at-mixin-Button();
|
||||
.at-mixin-ButtonColor('at-red', 'at-white');
|
||||
}
|
||||
|
||||
.at-ButtonHollow--white {
|
||||
.at-mixin-Button();
|
||||
.at-mixin-ButtonHollow('at-gray-dark-3x', 'at-gray-dark-2x');
|
||||
border-color: @at-gray-dark;
|
||||
}
|
||||
|
||||
.at-ButtonIcon {
|
||||
padding: @at-space-2x @at-space-4x;
|
||||
font-size: @at-font-size-3x;
|
||||
}
|
||||
83
awx/ui/client/lib/theme/_mixins.less
Normal file
83
awx/ui/client/lib/theme/_mixins.less
Normal file
@ -0,0 +1,83 @@
|
||||
.at-mixin-Placeholder (@color) {
|
||||
&:-moz-placeholder {
|
||||
color: @color;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
color: @color;
|
||||
}
|
||||
&::-webkit-input-placeholder {
|
||||
color: @color;
|
||||
}
|
||||
}
|
||||
|
||||
.at-mixin-Heading (@size) {
|
||||
color: @at-gray-dark-4x;
|
||||
font-size: @size;
|
||||
font-weight: @at-font-weight-2x;
|
||||
line-height: @at-line-height-short;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-mixin-Button () {
|
||||
height: @at-input-height;
|
||||
padding: @at-space-2x @at-space-4x;
|
||||
font-size: @at-font-size;
|
||||
}
|
||||
|
||||
.at-mixin-ButtonColor (@background, @color, @hover: '@{background}--hover') {
|
||||
background-color: @@background;
|
||||
|
||||
&, &:hover, &:focus {
|
||||
color: @@color;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: @@hover;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: fade(@@background, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.at-mixin-ButtonHollow (@color, @accent) {
|
||||
background-color: @at-white;
|
||||
color: @@color;
|
||||
border-color: @@color;
|
||||
|
||||
&:hover, &:active {
|
||||
color: @@color;
|
||||
background-color: @at-white--hover;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: @at-white;
|
||||
background-color: @@accent;
|
||||
border-color: @@accent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.at-mixin-ButtonIcon () {
|
||||
line-height: @at-line-height-short;
|
||||
color: @at-gray-dark-2x;
|
||||
|
||||
& > i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > i:hover {
|
||||
color: @at-gray-dark-3x;
|
||||
}
|
||||
}
|
||||
|
||||
.at-mixin-FontFixedWidth () {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
5
awx/ui/client/lib/theme/_temporary-overrides.less
Normal file
5
awx/ui/client/lib/theme/_temporary-overrides.less
Normal file
@ -0,0 +1,5 @@
|
||||
// TODO (remove override on cleanup):
|
||||
|
||||
.at-Panel-heading:hover {
|
||||
cursor: default;
|
||||
}
|
||||
18
awx/ui/client/lib/theme/_utility.less
Normal file
18
awx/ui/client/lib/theme/_utility.less
Normal file
@ -0,0 +1,18 @@
|
||||
.at-u-noSpace {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.at-u-flat {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.at-u-thin {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
72
awx/ui/client/lib/theme/_variables.less
Normal file
72
awx/ui/client/lib/theme/_variables.less
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* All variables used in the UI.
|
||||
*
|
||||
* 1. Colors
|
||||
* 2. Typography
|
||||
* 3. Layout
|
||||
* 4. Input
|
||||
* 5. Misc
|
||||
*/
|
||||
|
||||
// 1. Colors --------------------------------------------------------------------------------------
|
||||
@at-gray-light-5x: #fcfcfc;
|
||||
@at-gray-light-4x: #fafafa;
|
||||
@at-gray-light-3x: #f6f6f6;
|
||||
@at-gray-light-2x: #f2f2f2;
|
||||
@at-gray-light: #ebebeb;
|
||||
@at-gray: #e1e1e1;
|
||||
@at-gray-dark: #d7d7d7;
|
||||
@at-gray-dark-2x: #b7b7b7;
|
||||
@at-gray-dark-3x: #848992;
|
||||
@at-gray-dark-4x: #707070;
|
||||
@at-gray-dark-5x: #161b1f;
|
||||
|
||||
@at-white: #ffffff;
|
||||
@at-white--hover: #f2f2f2;
|
||||
|
||||
@at-blue: #337ab7;
|
||||
@at-blue--hover: #286090;
|
||||
|
||||
@at-green: #5cb85c;
|
||||
@at-green--hover: #449D44;
|
||||
|
||||
@at-yellow: #f0ad4e;
|
||||
@at-yellow--hover: #ec971f;
|
||||
|
||||
@at-red: #d9534f;
|
||||
@at-red--hover: #c9302c;
|
||||
|
||||
@at-redAlert: #ff0000;
|
||||
@at-redAlert--hover: #d81f1f;
|
||||
|
||||
// 2. Typography ----------------------------------------------------------------------------------
|
||||
@at-font-size: 12px;
|
||||
@at-font-size-2x: 13px;
|
||||
@at-font-size-3x: 14px;
|
||||
@at-font-size-4x: 16px;
|
||||
|
||||
@at-font-weight: 400;
|
||||
@at-font-weight-2x: 700;
|
||||
@at-font-weight-3x: 900;
|
||||
|
||||
@at-line-height-short: 0.9;
|
||||
@at-line-height-tall: 2;
|
||||
@at-line-height: 24px;
|
||||
|
||||
// 3. Layout --------------------------------------------------------------------------------------
|
||||
@at-space: 3px;
|
||||
@at-space-2x: 4px;
|
||||
@at-space-3x: 5px;
|
||||
@at-space-4x: 10px;
|
||||
@at-space-5x: 15px;
|
||||
@at-space-6x: 20px;
|
||||
|
||||
// 4. Input ---------------------------------------------------------------------------------------
|
||||
@at-input-button-width: 72px;
|
||||
@at-input-height: 30px;
|
||||
|
||||
// 5. Misc ----------------------------------------------------------------------------------------
|
||||
@at-border-radius: 5px;
|
||||
@at-popover-width: 320px;
|
||||
@at-inset-width: 5px;
|
||||
|
||||
15
awx/ui/client/lib/theme/index.less
Normal file
15
awx/ui/client/lib/theme/index.less
Normal file
@ -0,0 +1,15 @@
|
||||
// App-wide styles
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_utility';
|
||||
@import '_common';
|
||||
|
||||
// Aggregated component and feature specific styles
|
||||
@import '../components/_index';
|
||||
@import '../../features/_index';
|
||||
|
||||
/*
|
||||
* Temporary overrides used only during the transition away from old style
|
||||
* structure to new style structure. Overrides unwanted/uneeded rules.
|
||||
*/
|
||||
@import '_temporary-overrides';
|
||||
@ -6,7 +6,6 @@
|
||||
|
||||
export default ['$scope', 'ListDefinition', 'Dataset', 'Wait', 'Rest', 'ProcessErrors', 'Prompt', '$state',
|
||||
function($scope, list, Dataset, Wait, Rest, ProcessErrors, Prompt, $state) {
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
|
||||
@ -41,6 +41,7 @@ import portalMode from './portal-mode/main';
|
||||
import systemTracking from './system-tracking/main';
|
||||
import inventories from './inventories/main';
|
||||
import inventoryScripts from './inventory-scripts/main';
|
||||
import credentials from './credentials/main';
|
||||
import credentialTypes from './credential-types/main';
|
||||
import organizations from './organizations/main';
|
||||
import managementJobs from './management-jobs/main';
|
||||
@ -60,7 +61,6 @@ import login from './login/main';
|
||||
import activityStream from './activity-stream/main';
|
||||
import standardOut from './standard-out/main';
|
||||
import Templates from './templates/main';
|
||||
import credentials from './credentials/main';
|
||||
import jobs from './jobs/main';
|
||||
import teams from './teams/main';
|
||||
import users from './users/main';
|
||||
@ -72,6 +72,11 @@ import footer from './footer/main';
|
||||
import scheduler from './scheduler/main';
|
||||
import instanceGroups from './instance-groups/main';
|
||||
|
||||
import '../lib/components';
|
||||
import '../lib/models';
|
||||
import '../lib/services';
|
||||
import '../features';
|
||||
|
||||
var tower = angular.module('Tower', [
|
||||
// how to add CommonJS / AMD third-party dependencies:
|
||||
// 1. npm install --save package-name
|
||||
@ -101,6 +106,7 @@ var tower = angular.module('Tower', [
|
||||
systemTracking.name,
|
||||
inventories.name,
|
||||
inventoryScripts.name,
|
||||
credentials.name,
|
||||
credentialTypes.name,
|
||||
organizations.name,
|
||||
managementJobs.name,
|
||||
@ -118,7 +124,6 @@ var tower = angular.module('Tower', [
|
||||
standardOut.name,
|
||||
Templates.name,
|
||||
portalMode.name,
|
||||
credentials.name,
|
||||
jobs.name,
|
||||
teams.name,
|
||||
users.name,
|
||||
@ -131,6 +136,11 @@ var tower = angular.module('Tower', [
|
||||
'PromptDialog',
|
||||
'AWDirectives',
|
||||
'features',
|
||||
|
||||
'at.lib.components',
|
||||
'at.lib.models',
|
||||
'at.lib.services',
|
||||
'at.features',
|
||||
])
|
||||
|
||||
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', '$rootScope',
|
||||
'$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest',
|
||||
'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange',
|
||||
'OwnerChange', 'CredentialFormSave', '$state', 'CreateSelect2', 'i18n',
|
||||
function($scope, $rootScope, $log,
|
||||
$stateParams, CredentialForm, GenerateForm, Rest, ProcessErrors,
|
||||
ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange,
|
||||
OwnerChange, CredentialFormSave, $state, CreateSelect2, i18n) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
// Inject dynamic view
|
||||
var form = CredentialForm,
|
||||
defaultUrl = GetBasePath('credentials'),
|
||||
url;
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
// Load the list of options for Kind
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: defaultUrl,
|
||||
field: 'kind',
|
||||
variable: 'credential_kind_options'
|
||||
});
|
||||
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: defaultUrl,
|
||||
field: 'become_method',
|
||||
variable: 'become_options'
|
||||
});
|
||||
|
||||
CreateSelect2({
|
||||
element: '#credential_become_method',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
CreateSelect2({
|
||||
element: '#credential_kind',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
// apply form definition's default field values
|
||||
GenerateForm.applyDefaults(form, $scope);
|
||||
|
||||
$scope.keyEntered = false;
|
||||
$scope.permissionsTooltip = i18n._('Please save before assigning permissions');
|
||||
|
||||
// determine if the currently logged-in user may share this credential
|
||||
// previous commentary said: "$rootScope.current_user isn't available because a call to the config endpoint hasn't finished resolving yet"
|
||||
// I'm 99% sure this state's will never resolve block will be rejected if setup surrounding config endpoint hasn't completed
|
||||
if ($rootScope.current_user && $rootScope.current_user.is_superuser) {
|
||||
$scope.canShareCredential = true;
|
||||
} else {
|
||||
Rest.setUrl(GetBasePath('users') + `${$rootScope.current_user.id}/admin_of_organizations`);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.canShareCredential = (data.count) ? true : false;
|
||||
}).error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!Empty($stateParams.user_id)) {
|
||||
// Get the username based on incoming route
|
||||
$scope.owner = 'user';
|
||||
$scope.user = $stateParams.user_id;
|
||||
OwnerChange({ scope: $scope });
|
||||
url = GetBasePath('users') + $stateParams.user_id + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.user_username = data.username;
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve user. GET status: ' + status });
|
||||
});
|
||||
} else if (!Empty($stateParams.team_id)) {
|
||||
// Get the username based on incoming route
|
||||
$scope.owner = 'team';
|
||||
$scope.team = $stateParams.team_id;
|
||||
OwnerChange({ scope: $scope });
|
||||
url = GetBasePath('teams') + $stateParams.team_id + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.team_name = data.name;
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve team. GET status: ' + status });
|
||||
});
|
||||
} else {
|
||||
// default type of owner to a user
|
||||
$scope.owner = 'user';
|
||||
OwnerChange({ scope: $scope });
|
||||
}
|
||||
|
||||
$scope.$watch("ssh_key_data", function(val) {
|
||||
if (val === "" || val === null || val === undefined) {
|
||||
$scope.keyEntered = false;
|
||||
$scope.ssh_key_unlock_ask = false;
|
||||
$scope.ssh_key_unlock = "";
|
||||
} else {
|
||||
$scope.keyEntered = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Kind change
|
||||
$scope.kindChange = function() {
|
||||
KindChange({ scope: $scope, form: form, reset: true });
|
||||
};
|
||||
|
||||
$scope.becomeMethodChange = function() {
|
||||
BecomeMethodChange({ scope: $scope });
|
||||
};
|
||||
|
||||
// Save
|
||||
$scope.formSave = function() {
|
||||
if ($scope[form.name + '_form'].$valid) {
|
||||
CredentialFormSave({ scope: $scope, mode: 'add' });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.formCancel = function() {
|
||||
$state.go('credentials');
|
||||
};
|
||||
|
||||
// Password change
|
||||
$scope.clearPWConfirm = function(fld) {
|
||||
// If password value changes, make sure password_confirm must be re-entered
|
||||
$scope[fld] = '';
|
||||
$scope[form.name + '_form'][fld].$setValidity('awpassmatch', false);
|
||||
};
|
||||
|
||||
// Respond to 'Ask at runtime?' checkbox
|
||||
$scope.ask = function(fld, associated) {
|
||||
if ($scope[fld + '_ask']) {
|
||||
$scope[fld] = 'ASK';
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "text");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Hide");
|
||||
if (associated !== "undefined") {
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "password");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Show");
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
}
|
||||
} else {
|
||||
$scope[fld] = '';
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "password");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Show");
|
||||
if (associated !== "undefined") {
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "text");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Hide");
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Click clear button
|
||||
$scope.clear = function(fld, associated) {
|
||||
$scope[fld] = '';
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
$scope[form.name + '_form'].$setDirty();
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -1,344 +0,0 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default ['$scope', '$rootScope', '$location',
|
||||
'$stateParams', 'CredentialForm', 'Rest',
|
||||
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
|
||||
'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange',
|
||||
'CredentialFormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n',
|
||||
function($scope, $rootScope, $location,
|
||||
$stateParams, CredentialForm, Rest, ProcessErrors, ClearScope, Prompt,
|
||||
GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, CredentialFormSave, Wait,
|
||||
$state, CreateSelect2, Authorization, i18n) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
var defaultUrl = GetBasePath('credentials'),
|
||||
form = CredentialForm,
|
||||
base = $location.path().replace(/^\//, '').split('/')[0],
|
||||
master = {},
|
||||
id = $stateParams.credential_id;
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.id = id;
|
||||
$scope.$watch('credential_obj.summary_fields.user_capabilities.edit', function(val) {
|
||||
if (val === false) {
|
||||
$scope.canAdd = false;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.canShareCredential = false;
|
||||
Wait('start');
|
||||
if (!$rootScope.current_user) {
|
||||
Authorization.restoreUserInfo();
|
||||
}
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: defaultUrl,
|
||||
field: 'kind',
|
||||
variable: 'credential_kind_options',
|
||||
callback: 'choicesReadyCredential'
|
||||
});
|
||||
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: defaultUrl,
|
||||
field: 'become_method',
|
||||
variable: 'become_options'
|
||||
});
|
||||
|
||||
if ($rootScope.current_user && $rootScope.current_user.is_superuser) {
|
||||
$scope.canShareCredential = true;
|
||||
} else {
|
||||
Rest.setUrl(GetBasePath('users') + `${$rootScope.current_user.id}/admin_of_organizations`);
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.canShareCredential = (data.count) ? true : false;
|
||||
Wait('stop');
|
||||
}).error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to find if users is admin of org' + status });
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watch('organization', function(val) {
|
||||
if (val === undefined) {
|
||||
$scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.');
|
||||
} else {
|
||||
$scope.permissionsTooltip = '';
|
||||
}
|
||||
});
|
||||
|
||||
setAskCheckboxes();
|
||||
OwnerChange({ scope: $scope });
|
||||
$scope.$watch("ssh_key_data", function(val) {
|
||||
if (val === "" || val === null || val === undefined) {
|
||||
$scope.keyEntered = false;
|
||||
$scope.ssh_key_unlock_ask = false;
|
||||
$scope.ssh_key_unlock = "";
|
||||
} else {
|
||||
$scope.keyEntered = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setAskCheckboxes() {
|
||||
var fld, i;
|
||||
for (fld in form.fields) {
|
||||
if (form.fields[fld].type === 'sensitive' && $scope[fld] === 'ASK') {
|
||||
// turn on 'ask' checkbox for password fields with value of 'ASK'
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "text");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Hide");
|
||||
$("#" + fld + "-clear-btn").attr("disabled", "disabled");
|
||||
$scope[fld + '_ask'] = true;
|
||||
} else {
|
||||
$scope[fld + '_ask'] = false;
|
||||
$("#" + fld + "-clear-btn").removeAttr("disabled");
|
||||
}
|
||||
master[fld + '_ask'] = $scope[fld + '_ask'];
|
||||
}
|
||||
|
||||
// Set kind field to the correct option
|
||||
for (i = 0; i < $scope.credential_kind_options.length; i++) {
|
||||
if ($scope.kind === $scope.credential_kind_options[i].value) {
|
||||
$scope.kind = $scope.credential_kind_options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($scope.removeChoicesReady) {
|
||||
$scope.removeChoicesReady();
|
||||
}
|
||||
$scope.removeChoicesReady = $scope.$on('choicesReadyCredential', function() {
|
||||
// Retrieve detail record and prepopulate the form
|
||||
Rest.setUrl(defaultUrl + ':id/');
|
||||
Rest.get({ params: { id: id } })
|
||||
.success(function(data) {
|
||||
if (data && data.summary_fields &&
|
||||
data.summary_fields.organization &&
|
||||
data.summary_fields.organization.id) {
|
||||
$scope.needsRoleList = true;
|
||||
} else {
|
||||
$scope.needsRoleList = false;
|
||||
}
|
||||
|
||||
$scope.credential_name = data.name;
|
||||
|
||||
var i, fld;
|
||||
|
||||
|
||||
for (fld in form.fields) {
|
||||
if (data[fld] !== null && data[fld] !== undefined) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = $scope[fld];
|
||||
}
|
||||
if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
|
||||
if (!Empty($scope.user)) {
|
||||
$scope.owner = 'user';
|
||||
} else {
|
||||
$scope.owner = 'team';
|
||||
}
|
||||
master.owner = $scope.owner;
|
||||
|
||||
for (i = 0; i < $scope.become_options.length; i++) {
|
||||
if ($scope.become_options[i].value === data.become_method) {
|
||||
$scope.become_method = $scope.become_options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.become_method && $scope.become_method.value === "") {
|
||||
$scope.become_method = null;
|
||||
}
|
||||
master.become_method = $scope.become_method;
|
||||
|
||||
$scope.$watch('become_method', function(val) {
|
||||
if (val !== null) {
|
||||
if (val.value === "") {
|
||||
$scope.become_username = "";
|
||||
$scope.become_password = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (i = 0; i < $scope.credential_kind_options.length; i++) {
|
||||
if ($scope.credential_kind_options[i].value === data.kind) {
|
||||
$scope.kind = $scope.credential_kind_options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
KindChange({
|
||||
scope: $scope,
|
||||
form: form,
|
||||
reset: false
|
||||
});
|
||||
|
||||
master.kind = $scope.kind;
|
||||
|
||||
CreateSelect2({
|
||||
element: '#credential_become_method',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
CreateSelect2({
|
||||
element: '#credential_kind',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
switch (data.kind) {
|
||||
case 'aws':
|
||||
$scope.access_key = data.username;
|
||||
$scope.secret_key = data.password;
|
||||
master.access_key = $scope.access_key;
|
||||
master.secret_key = $scope.secret_key;
|
||||
break;
|
||||
case 'ssh':
|
||||
$scope.ssh_password = data.password;
|
||||
master.ssh_password = $scope.ssh_password;
|
||||
break;
|
||||
case 'rax':
|
||||
$scope.api_key = data.password;
|
||||
master.api_key = $scope.api_key;
|
||||
break;
|
||||
case 'gce':
|
||||
$scope.email_address = data.username;
|
||||
$scope.project = data.project;
|
||||
break;
|
||||
case 'azure':
|
||||
$scope.subscription = data.username;
|
||||
break;
|
||||
}
|
||||
$scope.credential_obj = data;
|
||||
|
||||
$scope.$emit('credentialLoaded');
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, form, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Failed to retrieve Credential: ' + $stateParams.id + '. GET status: ' + status
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Save changes to the parent
|
||||
$scope.formSave = function() {
|
||||
if ($scope[form.name + '_form'].$valid) {
|
||||
CredentialFormSave({ scope: $scope, mode: 'edit' });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Owner change
|
||||
$scope.ownerChange = function() {
|
||||
OwnerChange({ scope: $scope });
|
||||
};
|
||||
|
||||
// Handle Kind change
|
||||
$scope.kindChange = function() {
|
||||
KindChange({ scope: $scope, form: form, reset: true });
|
||||
};
|
||||
|
||||
$scope.becomeMethodChange = function() {
|
||||
BecomeMethodChange({ scope: $scope });
|
||||
};
|
||||
|
||||
$scope.formCancel = function() {
|
||||
$state.transitionTo('credentials');
|
||||
};
|
||||
|
||||
// Related set: Add button
|
||||
$scope.add = function(set) {
|
||||
$rootScope.flashMessage = null;
|
||||
$location.path('/' + base + '/' + $stateParams.id + '/' + set + '/add');
|
||||
};
|
||||
|
||||
// Related set: Edit button
|
||||
$scope.edit = function(set, id) {
|
||||
$rootScope.flashMessage = null;
|
||||
$location.path('/' + base + '/' + $stateParams.id + '/' + set + '/' + id);
|
||||
};
|
||||
|
||||
// Related set: Delete button
|
||||
$scope['delete'] = function(set, itm_id, name, title) {
|
||||
$rootScope.flashMessage = null;
|
||||
|
||||
var action = function() {
|
||||
var url = defaultUrl + id + '/' + set + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.post({
|
||||
id: itm_id,
|
||||
disassociate: 1
|
||||
})
|
||||
.success(function() {
|
||||
$('#prompt-modal').modal('hide');
|
||||
})
|
||||
.error(function(data, status) {
|
||||
$('#prompt-modal').modal('hide');
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. POST returned status: ' + status
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Prompt({
|
||||
hdr: i18n._('Delete'),
|
||||
body: '<div class="Prompt-bodyQuery">' + i18n.sprintf(i18n._('Are you sure you want to remove the %s below from %s?'), title, $scope.name) + '</div><div class="Prompt-bodyTarget">' + name + '</div>',
|
||||
action: action,
|
||||
actionText: i18n._('DELETE')
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Password change
|
||||
$scope.clearPWConfirm = function(fld) {
|
||||
// If password value changes, make sure password_confirm must be re-entered
|
||||
$scope[fld] = '';
|
||||
$scope[form.name + '_form'][fld].$setValidity('awpassmatch', false);
|
||||
};
|
||||
|
||||
// Respond to 'Ask at runtime?' checkbox
|
||||
$scope.ask = function(fld, associated) {
|
||||
if ($scope[fld + '_ask']) {
|
||||
$scope[fld] = 'ASK';
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "text");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Hide");
|
||||
if (associated !== "undefined") {
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "password");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Show");
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
}
|
||||
} else {
|
||||
$scope[fld] = '';
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "password");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Show");
|
||||
if (associated !== "undefined") {
|
||||
$("#" + form.name + "_" + fld + "_input").attr("type", "text");
|
||||
$("#" + form.name + "_" + fld + "_show_input_button").html("Hide");
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clear = function(fld, associated) {
|
||||
$scope[fld] = '';
|
||||
$scope[associated] = '';
|
||||
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
|
||||
$scope[form.name + '_form'].$setDirty();
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -6,15 +6,12 @@
|
||||
|
||||
import ownerList from './ownerList.directive';
|
||||
import CredentialsList from './list/credentials-list.controller';
|
||||
import CredentialsAdd from './add/credentials-add.controller';
|
||||
import CredentialsEdit from './edit/credentials-edit.controller';
|
||||
import BecomeMethodChange from './factories/become-method-change.factory';
|
||||
import CredentialFormSave from './factories/credential-form-save.factory';
|
||||
import KindChange from './factories/kind-change.factory';
|
||||
import OwnerChange from './factories/owner-change.factory';
|
||||
import CredentialList from './credentials.list';
|
||||
import CredentialForm from './credentials.form';
|
||||
import { N_ } from '../i18n';
|
||||
|
||||
export default
|
||||
angular.module('credentials', [])
|
||||
@ -24,38 +21,5 @@ export default
|
||||
.factory('KindChange', KindChange)
|
||||
.factory('OwnerChange', OwnerChange)
|
||||
.controller('CredentialsList', CredentialsList)
|
||||
.controller('CredentialsAdd', CredentialsAdd)
|
||||
.controller('CredentialsEdit', CredentialsEdit)
|
||||
.factory('CredentialList', CredentialList)
|
||||
.factory('CredentialForm', CredentialForm)
|
||||
.config(['$stateProvider', 'stateDefinitionsProvider',
|
||||
function($stateProvider, stateDefinitionsProvider) {
|
||||
let stateDefinitions = stateDefinitionsProvider.$get();
|
||||
|
||||
// lazily generate a tree of substates which will replace this node in ui-router's stateRegistry
|
||||
// see: stateDefinition.factory for usage documentation
|
||||
$stateProvider.state({
|
||||
name: 'credentials',
|
||||
url: '/credentials',
|
||||
lazyLoad: () => stateDefinitions.generateTree({
|
||||
parent: 'credentials',
|
||||
modes: ['add', 'edit'],
|
||||
list: 'CredentialList',
|
||||
form: 'CredentialForm',
|
||||
controllers: {
|
||||
list: CredentialsList,
|
||||
add: CredentialsAdd,
|
||||
edit: CredentialsEdit
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'credential'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'setup',
|
||||
label: N_('CREDENTIALS')
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
]);
|
||||
.factory('CredentialForm', CredentialForm);
|
||||
|
||||
5
awx/ui/client/test/index.js
Normal file
5
awx/ui/client/test/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import 'angular';
|
||||
import 'angular-mocks';
|
||||
|
||||
import '../components';
|
||||
import './panel.spec';
|
||||
47
awx/ui/client/test/karma.conf.js
Normal file
47
awx/ui/client/test/karma.conf.js
Normal file
@ -0,0 +1,47 @@
|
||||
let path = require('path');
|
||||
|
||||
module.exports = config => {
|
||||
config.set({
|
||||
basePath: '',
|
||||
singleRun: true,
|
||||
autoWatch: false,
|
||||
colors: true,
|
||||
frameworks: ['jasmine'],
|
||||
browsers: ['PhantomJS'],
|
||||
reporters: ['progress'],
|
||||
files: [
|
||||
'./index.js',
|
||||
'../components/**/*.html'
|
||||
],
|
||||
plugins: [
|
||||
'karma-webpack',
|
||||
'karma-jasmine',
|
||||
'karma-phantomjs-launcher',
|
||||
'karma-ng-html2js-preprocessor'
|
||||
],
|
||||
preprocessors: {
|
||||
'../components/**/*.html': 'ng-html2js',
|
||||
'../components/index.js': 'webpack',
|
||||
'./index.js': 'webpack'
|
||||
},
|
||||
ngHtml2JsPreprocessor: {
|
||||
moduleName: 'at.test.templates',
|
||||
stripPrefix: path.resolve(__dirname, '..'),
|
||||
prependPrefix: 'static/partials'
|
||||
},
|
||||
webpack: {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
webpackMiddleware: {
|
||||
noInfo: 'errors-only'
|
||||
}
|
||||
});
|
||||
};
|
||||
22
awx/ui/client/test/panel.spec.js
Normal file
22
awx/ui/client/test/panel.spec.js
Normal file
@ -0,0 +1,22 @@
|
||||
describe('Components | panel', () => {
|
||||
|
||||
let $compile;
|
||||
let $rootScope;
|
||||
|
||||
beforeEach(done => {
|
||||
angular.mock.module('at.components')
|
||||
angular.mock.module('at.test.templates');
|
||||
|
||||
inject((_$compile_, _$rootScope_) => {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should load the navigation partial', function() {
|
||||
var element = $compile('<at-panel></at-panel>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(element.html()).toContain('at-Panel');
|
||||
}); });
|
||||
@ -18,6 +18,7 @@ module.exports = {
|
||||
},
|
||||
keepalive: false,
|
||||
watchTask: true,
|
||||
reloadDebounce: 1000,
|
||||
// The browser-sync-client lib will write your current scroll position to window.name
|
||||
// https://github.com/BrowserSync/browser-sync-client/blob/a2718faa91e11553feca7a3962313bf1ec6ba3e5/dist/index.js#L500
|
||||
// This strategy is enabled in the core browser-sync lib, and not externally documented as an option. Yay!
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
module.exports = {
|
||||
dev: {
|
||||
tasks: ['copy:vendor', 'copy:assets', 'copy:partials', 'copy:languages', 'copy:config', 'less:dev'],
|
||||
tasks: ['copy:vendor', 'copy:assets', 'copy:partials', 'copy:views', 'copy:languages', 'copy:config', 'less:dev'],
|
||||
},
|
||||
// This concurrent target is intended for development ui builds that do not require raising browser-sync or filesystem polling
|
||||
devNoSync: {
|
||||
tasks: ['copy:vendor', 'copy:assets', 'copy:partials', 'copy:languages', 'copy:config', 'less:dev', 'webpack:dev'],
|
||||
tasks: ['copy:vendor', 'copy:assets', 'copy:partials', 'copy:views', 'copy:languages', 'copy:config', 'less:dev', 'webpack:dev'],
|
||||
},
|
||||
prod: {
|
||||
tasks: ['newer:copy:vendor', 'newer:copy:assets', 'newer:copy:partials', 'newer:copy:languages', 'newer:copy:config', 'newer:less:prod']
|
||||
tasks: ['newer:copy:vendor', 'newer:copy:assets', 'newer:copy:partials', 'newer:copy:views', 'newer:copy:languages', 'newer:copy:config', 'newer:less:prod']
|
||||
},
|
||||
watch: {
|
||||
tasks: ['watch:css', 'watch:partials', 'watch:assets', ['webpack:dev', 'watch:config']],
|
||||
tasks: ['watch:css', 'watch:partials', 'watch:views', 'watch:assets', ['webpack:dev', 'watch:config']],
|
||||
options: {
|
||||
logConcurrentOutput: true
|
||||
}
|
||||
|
||||
@ -30,6 +30,14 @@ module.exports = {
|
||||
dest: 'static/lib/'
|
||||
}]
|
||||
},
|
||||
views: {
|
||||
files: [{
|
||||
cwd: 'client/features',
|
||||
expand: true,
|
||||
src: ['**/*.view.html'],
|
||||
dest: 'static/views/'
|
||||
}]
|
||||
},
|
||||
partials: {
|
||||
files: [{
|
||||
cwd: 'client/src',
|
||||
@ -41,6 +49,11 @@ module.exports = {
|
||||
expand: true,
|
||||
src: ['*.html'],
|
||||
dest: 'static/partials/'
|
||||
}, {
|
||||
cwd: 'client/lib/components',
|
||||
expand: true,
|
||||
src: ['**/*.partial.html'],
|
||||
dest: 'static/partials/components/'
|
||||
}]
|
||||
},
|
||||
languages: {
|
||||
|
||||
@ -10,18 +10,19 @@ module.exports = {
|
||||
src: [
|
||||
'client/legacy-styles/*.less',
|
||||
'client/src/**/*.less',
|
||||
'client/lib/theme/index.less'
|
||||
]
|
||||
}],
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
|
||||
prod: {
|
||||
files: {
|
||||
'static/tower.min.css': [
|
||||
'client/legacy-styles/*.less',
|
||||
'client/src/**/*.less',
|
||||
'client/lib/theme/index.less'
|
||||
]
|
||||
},
|
||||
options: {
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
module.exports = {
|
||||
css: {
|
||||
files: 'client/**/*.less',
|
||||
tasks: ['newer:less:dev']
|
||||
tasks: ['less:dev']
|
||||
},
|
||||
partials: {
|
||||
files: 'client/src/**/*.html',
|
||||
files: [
|
||||
'client/lib/components/**/*.partial.html',
|
||||
'client/src/**/*.partial.html'
|
||||
],
|
||||
tasks: ['newer:copy:partials']
|
||||
},
|
||||
views: {
|
||||
files: 'client/features/**/*.view.html',
|
||||
tasks: ['newer:copy:views']
|
||||
},
|
||||
assets: {
|
||||
files: 'client/assets',
|
||||
tasks: ['newer:copy:assets']
|
||||
|
||||
@ -61,7 +61,11 @@ module.exports = function(config) {
|
||||
}, {
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [path.resolve() + '/client/src/'],
|
||||
include: [
|
||||
path.resolve() + '/client/src/',
|
||||
path.resolve() + '/client/lib/',
|
||||
path.resolve() + '/client/features/'
|
||||
],
|
||||
exclude: '/(node_modules)/',
|
||||
query: {
|
||||
presets: ['es2015'],
|
||||
|
||||
@ -24,7 +24,10 @@
|
||||
"test": "karma start karma.conf.js",
|
||||
"jshint": "grunt clean:jshint jshint:source --no-color",
|
||||
"test:ci": "npm run test -- --single-run --reporter junit,dots --browsers=PhantomJS",
|
||||
"lint": "./node_modules/.bin/eslint -c .eslintrc.js ."
|
||||
"lint": "./node_modules/.bin/eslint -c .eslintrc.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": {
|
||||
"browser-sync": "^2.14.0",
|
||||
@ -67,6 +70,7 @@
|
||||
"karma-html2js-preprocessor": "^1.0.0",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-ng-html2js-preprocessor": "^1.0.0",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"karma-sauce-launcher": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user