add credential types to ui

This commit is contained in:
John Mitchell 2017-04-24 16:13:48 -04:00
parent 46bd02b4c6
commit 38c0c0a89b
15 changed files with 730 additions and 9 deletions

View File

@ -2152,6 +2152,7 @@ tr td button i {
cursor: not-allowed;
opacity: 100;
background: @egrey;
border-radius: 5px;
}
.select2-container--default .select2-selection--single {
@ -2337,6 +2338,7 @@ input[disabled].ui-spinner-input {
margin-right: 0;
overflow: auto !important;
overflow-y: auto !important;
border-radius: 5px;
}
.CodeMirror-lines {

View File

@ -39,11 +39,9 @@
// Disabled
textarea[disabled="disabled"] + div[id*="-container"]{
pointer-events: none;
cursor: not-allowed;
.CodeMirror {
cursor: not-allowed;
pointer-events: none;
}
.CodeMirror.cm-s-default,
@ -68,4 +66,10 @@ textarea[disabled="disabled"] + div[id*="-container"]{
.CodeMirror-cursors {
display: none;
}
.CodeMirror.cm-s-default {
min-height: initial !important;
max-height: initial !important;
height: initial !important;
}
}

View File

@ -283,13 +283,13 @@
}
.Button-primary--hollow {
border: 1px solid @default-link;
border: 1px solid @default-link;
color: @default-link;
background: @default-bg;
}
.Button-primary--hollow:hover {
color: @default-link-hov;
border: 1px solid @default-link-hov;
border: 1px solid @default-link-hov;
}
.ui-spinner{
@ -495,7 +495,8 @@ input[type='radio']:checked:before {
display: block !important;
}
.Form-inputLabelContainer[for=variables] {
.Form-inputLabelContainer[for=variables],
.Form-inputLabelContainer--codeMirror {
width: auto;
display: inline-block !important;
}
@ -506,9 +507,10 @@ input[type='radio']:checked:before {
.FormToggle {}
.FormToggle-container {
float: right;
margin: 0 0 0 10px;
padding-bottom: 5px;
display: initial;
padding-bottom: 5px;
label {
&:first-child {
@ -529,6 +531,10 @@ input[type='radio']:checked:before {
}
}
#credential_type_form .FormToggle-container {
float: initial;
}
.Form-inputLabelContainer {
width: 100%;
display: block !important;

View File

@ -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 credentialTypes from './credential-types/main';
import organizations from './organizations/main';
import managementJobs from './management-jobs/main';
import workflowResults from './workflow-results/main';
@ -99,6 +100,7 @@ var tower = angular.module('Tower', [
systemTracking.name,
inventories.name,
inventoryScripts.name,
credentialTypes.name,
organizations.name,
managementJobs.name,
setupMenu.name,

View File

@ -0,0 +1,106 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['Rest', 'Wait',
'CredentialTypesForm', 'ProcessErrors', 'GetBasePath',
'GenerateForm', '$scope', '$state', 'Alert', 'GetChoices', 'ParseTypeChange', 'ToJSON', 'CreateSelect2',
function(Rest, Wait,
CredentialTypesForm, ProcessErrors, GetBasePath,
GenerateForm, $scope, $state, Alert, GetChoices, ParseTypeChange, ToJSON, CreateSelect2
) {
var form = CredentialTypesForm,
url = GetBasePath('credential_types');
init();
function init() {
// Load the list of options for Kind
GetChoices({
scope: $scope,
url: url,
field: 'kind',
variable: 'credential_kind_options'
});
Rest.setUrl(url);
Rest.options()
.success(function(data) {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a credential type.', 'alert-info');
}
});
// apply form definition's default field values
GenerateForm.applyDefaults(form, $scope);
// @issue @jmitchell - this setting probably collides with new RBAC can* implementation?
$scope.canEdit = true;
var callback = function() {
// Make sure the form controller knows there was a change
$scope[form.name + '_form'].$setDirty();
};
$scope.parseTypeInputs = 'yaml';
$scope.parseTypeInjectors = 'yaml';
ParseTypeChange({
scope: $scope,
field_id: 'credential_type_inputs',
variable: 'inputs',
onChange: callback,
parse_variable: 'parseTypeInputs'
});
ParseTypeChange({
scope: $scope,
field_id: 'credential_type_injectors',
variable: 'injectors',
onChange: callback,
parse_variable: 'parseTypeInjectors'
});
CreateSelect2({
element: '#credential_type_kind',
multiple: false,
});
}
// Save
$scope.formSave = function() {
GenerateForm.clearApiErrors($scope);
Wait('start');
Rest.setUrl(url);
var inputs = ToJSON($scope.parseTypeInputs, $scope.inputs);
var injectors = ToJSON($scope.parseTypeInjectors, $scope.injectors);
if (inputs === null) {
inputs = {};
}
if (injectors === null) {
injectors = {};
}
Rest.post({
name: $scope.name,
description: $scope.description,
kind: $scope.kind.value,
inputs: inputs,
injectors: injectors
})
.success(function(data) {
$state.go('credentialTypes.edit', { credential_type_id: data.id }, { reload: true });
Wait('stop');
})
.error(function(data, status) {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to add new credential type. PUT returned status: ' + status
});
});
};
$scope.formCancel = function() {
$state.go('^');
};
}
];

View File

@ -0,0 +1,11 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import controller from './add.controller';
export default
angular.module('credentialTypesAdd', [])
.controller('CredentialTypesAddController', controller);

View File

@ -0,0 +1,113 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name forms.function:CredentialType
* @description This form is for adding/editing a credential type
*/
export default ['i18n', function(i18n) {
return {
addTitle: i18n._('NEW CREDENTIAL TYPE'),
editTitle: '{{ name }}',
name: 'credential_type',
basePath: 'credential_types',
stateTree: 'credentialTypes',
breadcrumbName: i18n._('CREDENTIAL TYPE'),
showActions: true,
// TODO: update fields to be the schema for credential types instead of inventory scripts
fields: {
name: {
label: i18n._('Name'),
type: 'text',
ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)',
required: true,
capitalize: false
},
description: {
label: i18n._('Description'),
type: 'text',
ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
kind: {
label: i18n._('Kind'),
excludeModal: true,
type: 'select',
ngOptions: 'kind.label for kind in credential_kind_options track by kind.value',
required: true,
awPopOver: '<dl>\n' +
'<dt>' + i18n._('Machine') + '</dt>\n' +
'<dd>' + i18n._('Authentication for remote machine access. This can include SSH keys, usernames, passwords, ' +
'and sudo information. Machine credentials are used when submitting jobs to run playbooks against ' +
'remote hosts.') + '</dd>' +
'<dt>' + i18n._('Network') + '</dt>\n' +
'<dd>' + i18n._('Authentication for network device access. This can include SSH keys, usernames, passwords, ' +
'and authorize information. Network credentials are used when submitting jobs to run playbooks against ' +
'network devices.') + '</dd>' +
'<dt>' + i18n._('Source Control') + '</dt>\n' +
'<dd>' + i18n._('Used to check out and synchronize playbook repositories with a remote source control ' +
'management system such as Git, Subversion (svn), or Mercurial (hg). These credentials are ' +
'used by Projects.') + '</dd>\n' +
'<dt>' + i18n._('Cloud') + '</dt>\n' +
'<dd>' + i18n._('Usernames, passwords, and access keys for authenticating to the specified cloud or infrastructure ' +
'provider. These are used for dynamic inventory sources and for cloud provisioning and deployment ' +
'in playbook runs.') + '</dd>\n' +
'</dl>\n',
dataTitle: i18n._('Kind'),
dataPlacement: 'right',
dataContainer: "body",
ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
inputs: {
label: i18n._('Input Configuration'),
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
type: 'textarea',
rows: 6,
default: '---',
showParseTypeToggle: true,
parseTypeName: 'parseTypeInputs',
awPopOver: '<p>TODO: input config helper text</p>',
dataTitle: i18n._('Input Configuration'),
dataPlacement: 'right',
dataContainer: "body",
ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
injectors: {
label: i18n._('Injector Configuration'),
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
type: 'textarea',
rows: 6,
default: '---',
showParseTypeToggle: true,
parseTypeName: 'parseTypeInjectors',
awPopOver: '<p>TODO: injector config helper text</p>',
dataTitle: i18n._('Injector Configuration'),
dataPlacement: 'right',
dataContainer: "body",
ngDisabled: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
},
buttons: { //for now always generates <button> tags
cancel: {
ngClick: 'formCancel()',
ngShow: '(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(credential_type.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: 'credential_type_form.$invalid', //Disable when $invalid, optional
ngShow: '(credential_type.summary_fields.user_capabilities.edit || canAdd)'
}
}
};
}];

View File

@ -0,0 +1,82 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['i18n', function(i18n){
return {
name: 'credential_types' ,
listTitle: i18n._('CREDENTIAL TYPES'),
basePath: 'credential_types',
iterator: 'credential_type',
index: false,
hover: false,
fields: {
name: {
key: true,
label: i18n._('Name'),
columnClass: 'col-md-3 col-sm-9 col-xs-9',
modalColumnClass: 'col-md-8'
},
// TODO: update to tooltip on name
description: {
label: i18n._('Description'),
excludeModal: true,
columnClass: 'col-md-4 hidden-sm hidden-xs'
},
kind: {
label: i18n._('Type'),
ngBind: 'credential_type.kind_label',
excludeModal: true,
columnClass: 'col-md-2 hidden-sm hidden-xs'
},
},
actions: {
add: {
mode: 'all', // One of: edit, select, all
ngClick: 'addCredentialType()',
awToolTip: i18n._('Create a new credential type'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ' + i18n._('ADD'),
ngShow: 'canAdd'
}
},
fieldActions: {
columnClass: 'col-md-2 col-sm-3 col-xs-3',
edit: {
ngClick: "editCredentialType(credential_type.id)",
icon: 'fa-edit',
label: i18n._('Edit'),
"class": 'btn-sm',
awToolTip: i18n._('Edit credenital type'),
dataPlacement: 'top',
ngShow: 'credential_type.summary_fields.user_capabilities.edit'
},
view: {
ngClick: "editCredentialType(credential_type.id)",
label: i18n._('View'),
"class": 'btn-sm',
awToolTip: i18n._('View credential type'),
dataPlacement: 'top',
ngShow: '!credential_type.summary_fields.user_capabilities.edit'
},
"delete": {
ngClick: "deleteCredentialType(credential_type.id, credential_type.name)",
icon: 'fa-trash',
label: i18n._('Delete'),
"class": 'btn-sm',
awToolTip: i18n._('Delete credential type'),
dataPlacement: 'top',
ngShow: 'credential_type.summary_fields.user_capabilities.delete'
}
}
};
}];

View File

@ -0,0 +1,172 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['Rest', 'Wait',
'CredentialTypesForm', 'ProcessErrors', 'GetBasePath',
'GenerateForm', 'credential_typeData',
'$scope', '$state', 'GetChoices', 'ParseTypeChange', 'ToJSON', 'ParseVariableString', 'CreateSelect2',
function(
Rest, Wait, CredentialTypesForm, ProcessErrors, GetBasePath,
GenerateForm, credential_typeData,
$scope, $state, GetChoices, ParseTypeChange, ToJSON, ParseVariableString, CreateSelect2
) {
var generator = GenerateForm,
data = credential_typeData,
id = credential_typeData.id,
form = CredentialTypesForm,
master = {},
url = GetBasePath('credential_types');
init();
function init() {
// Load the list of options for Kind
GetChoices({
scope: $scope,
url: url,
field: 'kind',
variable: 'credential_kind_options',
callback: 'choicesReadyCredentialTypes'
});
}
if ($scope.removeChoicesReady) {
$scope.removeChoicesReady();
}
$scope.removeChoicesReady = $scope.$on('choicesReadyCredentialTypes',
function() {
$scope.credential_type = credential_typeData;
$scope.$watch('credential_type.summary_fields.user_capabilities.edit', function(val) {
if (val === false) {
$scope.canAdd = false;
}
});
function getVars(str){
// Quick function to test if the host vars are a json-object-string,
// by testing if they can be converted to a JSON object w/o error.
function IsJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
if(str === ''){
return '---';
}
else if(IsJsonString(str)){
str = JSON.parse(str);
return jsyaml.safeDump(str);
}
else if(!IsJsonString(str)){
return str;
}
}
var fld, i;
for (fld in form.fields) {
if (data[fld] && fld !== 'inputs' || fld !== 'injectors') {
$scope[fld] = data[fld];
master[fld] = data[fld];
}
if (fld === "kind") {
// 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];
master[fld] = $scope.credential_kind_options[i];
break;
}
}
}
}
$scope.inputs = ParseVariableString(getVars(data.inputs));
$scope.injectors = ParseVariableString(getVars(data.injectors));
// if ($scope.inputs === "{}") {
// $scope.inputs = "---";
// }
//
// if ($scope.injectors === "{}") {
// $scope.injectors = "---";
// }
// $scope.inputs = JSON.parse($scope.inputs);
// $scope.injectors = JSON.parse($scope.injectors);
var callback = function() {
// Make sure the form controller knows there was a change
$scope[form.name + '_form'].$setDirty();
};
$scope.parseTypeInputs = 'yaml';
$scope.parseTypeInjectors = 'yaml';
ParseTypeChange({
scope: $scope,
field_id: 'credential_type_inputs',
variable: 'inputs',
onChange: callback,
parse_variable: 'parseTypeInputs'
});
ParseTypeChange({
scope: $scope,
field_id: 'credential_type_injectors',
variable: 'injectors',
onChange: callback,
parse_variable: 'parseTypeInjectors'
});
CreateSelect2({
element: '#credential_type_kind',
multiple: false,
});
}
);
$scope.formSave = function() {
generator.clearApiErrors($scope);
Wait('start');
Rest.setUrl(url + id + '/');
var inputs = ToJSON($scope.parseTypeInputs, $scope.inputs);
var injectors = ToJSON($scope.parseTypeInjectors, $scope.injectors);
if (inputs === null) {
inputs = {};
}
if (injectors === null) {
injectors = {};
}
Rest.put({
name: $scope.name,
description: $scope.description,
kind: $scope.kind.value,
inputs: inputs,
injectors: injectors
})
.success(function() {
$state.go($state.current, null, { reload: true });
Wait('stop');
})
.error(function(data, status) {
ProcessErrors($scope, data, status, form, {
hdr: 'Error!',
msg: 'Failed to add new credential type. PUT returned status: ' + status
});
});
};
$scope.formCancel = function() {
$state.go('credentialTypes');
};
}
];

View File

@ -0,0 +1,11 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import controller from './edit.controller';
export default
angular.module('credentialTypesEdit', [])
.controller('CredentialTypesEditController', controller);

View File

@ -0,0 +1,122 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$rootScope', '$scope', 'Wait', 'CredentialTypesList',
'GetBasePath', 'Rest', 'ProcessErrors', 'Prompt', '$state', '$filter', 'Dataset', 'rbacUiControlService', 'Alert',
function(
$rootScope, $scope, Wait, CredentialTypesList,
GetBasePath, Rest, ProcessErrors, Prompt, $state, $filter, Dataset, rbacUiControlService, Alert
) {
var defaultUrl = GetBasePath('credential_types'),
list = CredentialTypesList;
init();
function init() {
if (!($rootScope.user_is_superuser || $rootScope.user_is_system_auditor)) {
$state.go("setup");
Alert('Permission Error', 'You do not have permission to view, edit or create custom credential types.', 'alert-info');
}
$scope.canAdd = false;
rbacUiControlService.canAdd("credential_types")
.then(function(canAdd) {
$scope.canAdd = canAdd;
});
// search init
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
}
// @todo what is going on here, and if it needs to happen in this controller make $rootScope var name more explicit
if ($rootScope.addedItem) {
$scope.addedItem = $rootScope.addedItem;
delete $rootScope.addedItem;
}
$scope.editCredentialType = function() {
$state.go('credentialTypes.edit', {
credential_type_id: this.credential_type.id
});
};
$scope.deleteCredentialType = function(id, name) {
var action = function() {
$('#prompt-modal').modal('hide');
Wait('start');
var url = defaultUrl + id + '/';
Rest.setUrl(url);
Rest.destroy()
.success(function() {
if (parseInt($state.params.credential_type_id) === id) {
$state.go('^', null, { reload: true });
} else {
$state.go('.', null, { reload: true });
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, {
hdr: 'Error!',
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status
});
});
};
var bodyHtml = '<div class="Prompt-bodyQuery">Are you sure you want to delete the credential type below?</div><div class="Prompt-bodyTarget">' + $filter('sanitize')(name) + '</div>';
Prompt({
hdr: 'Delete',
body: bodyHtml,
action: action,
actionText: 'DELETE'
});
};
$scope.addCredentialType = function() {
$state.go('credentialTypes.add');
};
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
if($scope.list.name === 'credential_types'){
if ($scope[list.name] !== undefined) {
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
// Set the item type label
if (list.fields.kind && $scope.options &&
$scope.options.hasOwnProperty('kind')) {
$scope.options.kind.choices.forEach(function(choice) {
if (choice[0] === item.kind) {
itm.kind_label = choice[1];
}
});
}
});
}
}
}
Rest.setUrl(GetBasePath("credential_types"));
Rest.options()
.success(function(data) {
$scope.options = data.actions.GET;
optionsRequestDataProcessing();
});
$scope.$watchCollection(`${$scope.list.name}`, function() {
optionsRequestDataProcessing();
}
);
}
];

View File

@ -0,0 +1,11 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import CredentialTypesListController from './list.controller';
export default
angular.module('credenitalTypesList', [])
.controller('CredentialTypesListController', CredentialTypesListController);

View File

@ -0,0 +1,71 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import credentialTypesList from './list/main';
import credentialTypesAdd from './add/main';
import credentialTypesEdit from './edit/main';
import list from './credential-types.list';
import form from './credential-types.form';
import { N_ } from '../i18n';
export default
angular.module('credentialTypes', [
credentialTypesList.name,
credentialTypesAdd.name,
credentialTypesEdit.name
])
.factory('CredentialTypesList', list)
.factory('CredentialTypesForm', form)
.config(['$stateProvider', 'stateDefinitionsProvider',
function($stateProvider, stateDefinitionsProvider) {
let stateDefinitions = stateDefinitionsProvider.$get();
$stateProvider.state({
name: 'credentialTypes',
url: '/credential_type',
lazyLoad: () => stateDefinitions.generateTree({
parent: 'credentialTypes',
modes: ['add', 'edit'],
list: 'CredentialTypesList',
form: 'CredentialTypesForm',
controllers: {
list: 'CredentialTypesListController',
add: 'CredentialTypesAddController',
edit: 'CredentialTypesEditController'
},
resolve: {
edit: {
credential_typeData: ['$state', '$stateParams', 'Rest', 'GetBasePath', 'ProcessErrors',
function($state, $stateParams, rest, getBasePath, ProcessErrors) {
var credentialTypeId = $stateParams.credential_type_id;
var url = getBasePath('credential_types') + credentialTypeId + '/';
rest.setUrl(url);
return rest.get()
.then(function(data) {
return data.data;
}).catch(function(response) {
ProcessErrors(null, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get credential type info. GET returned status: ' +
response.status
});
});
}
]
}
},
data: {
activityStream: true,
activityStreamTarget: 'custom_inventory_script' // TODO: change to 'credential_type'...there's probably more work that needs to be done to hook up activity stream
},
ncyBreadcrumb: {
parent: 'setup',
label: N_('CREDENTIAL TYPES')
}
})
});
}
]);

View File

@ -43,6 +43,13 @@
Create templates for sending notifications with Email, HipChat, Slack, and SMS.
</p>
</a>
<a ui-sref="credentialTypes" class="SetupItem"
ng-if="user_is_system_auditor || user_is_superuser">
<h4 class="SetupItem-title" translate>Credential Types</h4>
<p class="SetupItem-description" translate>
Create custom credential types to be used for authenticating to network hosts and cloud sources
</p>
</a>
<a ui-sref="license" class="SetupItem">
<h4 class="SetupItem-title" translate>View Your License</h4>
<p class="SetupItem-description" translate>

View File

@ -646,6 +646,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += (field.labelClass) ? field.labelClass : "";
html += `${field.required ? ' prepend-asterisk ' : ''}`;
html += (horizontal) ? " " + getLabelWidth() : "Form-inputLabelContainer ";
html += (field.showParseTypeToggle) ? "Form-inputLabelContainer--codeMirror " : "";
html += "\" ";
html += (field.labelNGClass) ? "ng-class=\"" + field.labelNGClass + "\" " : "";
html += "for=\"" + fld + '">\n';
@ -658,7 +659,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += (field.awPopOver && !field.awPopOverRight) ? Attr(field, 'awPopOver', fld) : "";
html += (field.hintText) ? "\n\t\t<span class=\"label-hint-text\">\n\t\t\t<i class=\"fa fa-info-circle\">\n\t\t\t</i>\n\t\t\tHint: " + field.hintText + "\n\t\t</span>" : "";
// Variable editing
if (fld === "variables" || fld === "extra_vars" || _.last(fld.split('_')) === 'variables' || fld === 'source_vars') {
if (fld === "variables" || fld === "extra_vars" || _.last(fld.split('_')) === 'variables' || fld === 'source_vars' || field.showParseTypeToggle === true) {
let parseTypeId = `${form.name}_${fld}_parse_type`;
let parseTypeName = field.parseTypeName || 'parseType';
let getToggleClass = (primary, secondary) => `{
@ -674,7 +675,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
<label ng-class="${toggleLeftClass}" class="btn btn-xs">
<input type="radio" value="yaml" ng-model="${parseTypeName}" ng-change="parseTypeChange('${parseTypeName}', '${fld}')" />YAML
</label>
<label ng-class="${toggleRightClass}" class="btn btn-xs">
<label ng-class="${toggleRightClass}" class="btn btn-xs btn-default">
<input type="radio" value="json" ng-model="${parseTypeName}" ng-change="parseTypeChange('${parseTypeName}', '${fld}')" />JSON
</label>
</div>