diff --git a/packages/core/src/forms/forms.module.js b/packages/core/src/forms/forms.module.js index 39b128285a4..c4ff56b08da 100644 --- a/packages/core/src/forms/forms.module.js +++ b/packages/core/src/forms/forms.module.js @@ -7,6 +7,7 @@ import { CORE_FORMS_CHECKLIST_CHECKLIST_DIRECTIVE } from './checklist/checklist. import { CORE_FORMS_CHECKMAP_CHECKMAP_DIRECTIVE } from './checkmap/checkmap.directive'; import { CORE_FORMS_IGNOREEMPTYDELETE_DIRECTIVE } from './ignoreEmptyDelete.directive'; import { CORE_FORMS_MAPEDITOR_MAPEDITOR_COMPONENT } from './mapEditor/mapEditor.component'; +import { CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT } from './mapObjectEditor/mapObjectEditor.component'; import { NUMBER_LIST_COMPONENT } from './numberList/numberList.component'; import { CORE_FORMS_VALIDATEONSUBMIT_VALIDATEONSUBMIT_DIRECTIVE } from './validateOnSubmit/validateOnSubmit.directive'; @@ -18,6 +19,7 @@ module(CORE_FORMS_FORMS_MODULE, [ CORE_FORMS_CHECKMAP_CHECKMAP_DIRECTIVE, CORE_FORMS_IGNOREEMPTYDELETE_DIRECTIVE, CORE_FORMS_MAPEDITOR_MAPEDITOR_COMPONENT, + CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT, CORE_FORMS_VALIDATEONSUBMIT_VALIDATEONSUBMIT_DIRECTIVE, NUMBER_LIST_COMPONENT, ]); diff --git a/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.html b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.html new file mode 100644 index 00000000000..c0a0c6b3f58 --- /dev/null +++ b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.html @@ -0,0 +1,56 @@ +
+
+ {{ $ctrl.label }} +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ {{ $ctrl.keyLabel }} + + +
Duplicate key
+
+ {{ $ctrl.valueLabel }} + + + + +
+ +
+
diff --git a/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.js b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.js new file mode 100644 index 00000000000..651e0980627 --- /dev/null +++ b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.js @@ -0,0 +1,113 @@ +'use strict'; + +import * as angular from 'angular'; +import { isString } from 'lodash'; + +import { CORE_VALIDATION_VALIDATEUNIQUE_DIRECTIVE } from '../../validation/validateUnique.directive'; + +import './mapObjectEditor.component.less'; + +export const CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT = 'spinnaker.core.forms.mapObjectEditor.component'; +export const name = CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT; // for backwards compatibility +angular + .module(CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT, [CORE_VALIDATION_VALIDATEUNIQUE_DIRECTIVE]) + .directive('jsonText', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attr, ngModel) { + function into(input) { + return JSON.parse(input); + } + function out(data) { + return JSON.stringify(data, null, 2); + } + ngModel.$parsers.push(into); + ngModel.$formatters.push(out); + }, + }; + }) + .component('mapObjectEditor', { + bindings: { + model: '=', + keyLabel: '@', + valueLabel: '@', + addButtonLabel: '@', + allowEmpty: '=?', + onChange: '&', + labelsLeft: ' Object.keys(this.model); + + this.addField = () => { + this.backingModel.push({ key: '', value: {}, checkUnique: modelKeys() }); + // do not fire the onChange event, since no values have been committed to the object + }; + + this.removeField = (index) => { + this.backingModel.splice(index, 1); + this.synchronize(); + this.onChange(); + }; + + // Clears existing values from model, then replaces them + this.synchronize = () => { + if (this.isParameterized) { + return; + } + const modelStart = JSON.stringify(this.model); + const allKeys = this.backingModel.map((pair) => pair.key); + modelKeys().forEach((key) => delete this.model[key]); + this.backingModel.forEach((pair) => { + if (pair.key && (this.allowEmpty || pair.value)) { + try { + // Parse value if it is a valid JSON object + this.model[pair.key] = JSON.parse(pair.value); + } catch (e) { + // If value is not a valid JSON object, just store the raw value + this.model[pair.key] = pair.value; + } + } + // include other keys to verify no duplicates + pair.checkUnique = allKeys.filter((key) => pair.key !== key); + }); + if (modelStart !== JSON.stringify(this.model)) { + this.onChange(); + } + }; + + // In Angular 1.7 Directive bindings were removed in the constructor, default values now must be instantiated within $onInit + // See https://docs.angularjs.org/guide/migration#-compile- and https://docs.angularjs.org/guide/migration#migrate1.5to1.6-ng-services-$compile + this.$onInit = () => { + // Set default values for optional fields + this.onChange = this.onChange || angular.noop; + this.keyLabel = this.keyLabel || 'Key'; + this.valueLabel = this.valueLabel || 'Value'; + this.addButtonLabel = this.addButtonLabel || 'Add Field'; + this.allowEmpty = this.allowEmpty || false; + this.labelsLeft = this.labelsLeft || false; + this.tableClass = this.label ? '' : 'no-border-top'; + this.columnCount = this.labelsLeft ? 5 : 3; + this.model = this.model || {}; + this.isParameterized = isString(this.model); + this.hiddenKeys = this.hiddenKeys || []; + + if (this.model && !this.isParameterized) { + modelKeys().forEach((key) => { + this.backingModel.push({ key: key, value: this.model[key] }); + }); + } + }; + + $scope.$watch(() => JSON.stringify(this.backingModel), this.synchronize); + }, + ], + templateUrl: require('./mapObjectEditor.component.html'), + }); diff --git a/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.less b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.less new file mode 100644 index 00000000000..bb67f98d869 --- /dev/null +++ b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.less @@ -0,0 +1,9 @@ +map-object-editor { + .table.no-border-top { + border-top: 2px solid var(--color-white); + + .table-label { + padding: 0.8rem 0 0 1rem; + } + } +} diff --git a/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.spec.js b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.spec.js new file mode 100644 index 00000000000..41be1ae763f --- /dev/null +++ b/packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.spec.js @@ -0,0 +1,85 @@ +'use strict'; + +describe('Component: mapObjectEditor', function () { + var scope; + + beforeEach(window.module(require('./mapObjectEditor.component').name)); + + beforeEach( + window.inject(function ($rootScope, $compile) { + scope = $rootScope.$new(); + this.compile = $compile; + }), + ); + + it('initializes with provided values', function () { + scope.model = { foo: { bar: 'baz' }, bah: 11 }; + let dom = this.compile('')(scope); + scope.$digest(); + + expect(dom.find('input').length).toBe(2); + expect(dom.find('textarea').length).toBe(2); + + expect(dom.find('input').get(0).value).toBe('foo'); + expect(dom.find('textarea').get(0).value).toBe(JSON.stringify({ bar: 'baz' }, null, 2)); + expect(dom.find('input').get(1).value).toBe('bah'); + expect(dom.find('textarea').get(1).value).toBe('11'); + }); + + describe('adding new entries', function () { + it('creates a new row in the table, but does not synchronize to model', function () { + scope.model = {}; + let dom = this.compile('')(scope); + scope.$digest(); + dom.find('button').click(); + expect(dom.find('tbody tr').length).toBe(1); + expect(dom.find('input').length).toBe(1); + expect(dom.find('textarea').length).toBe(1); + }); + + it('does not flag multiple new rows without keys as having duplicate keys', function () { + scope.model = {}; + let dom = this.compile('')(scope); + scope.$digest(); + dom.find('button').click(); + dom.find('button').click(); + + expect(dom.find('tbody tr').length).toBe(2); + expect(dom.find('input').length).toBe(2); + expect(dom.find('textarea').length).toBe(2); + + expect(dom.find('.error-message').length).toBe(0); + }); + }); + + describe('removing entries', function () { + it('removes the entry when the trash can is clicked', function () { + scope.model = { foo: { bar: 'baz' } }; + let dom = this.compile('')(scope); + scope.$digest(); + + expect(dom.find('input').length).toBe(1); + expect(dom.find('textarea').length).toBe(1); + + dom.find('a').click(); + + expect(dom.find('tbody tr').length).toBe(0); + expect(dom.find('input').length).toBe(0); + expect(dom.find('textarea').length).toBe(0); + expect(scope.model.foo).toBeUndefined(); + }); + }); + + describe('duplicate key handling', function () { + it('provides a warning when a duplicate key is entered', function () { + scope.model = { a: { bar: 'baz' }, b: '2' }; + let dom = this.compile('')(scope); + scope.$digest(); + + $(dom.find('input')[1]).val('a').trigger('input'); + scope.$digest(); + + expect(dom.find('.error-message').length).toBe(1); + }); + }); +}); diff --git a/packages/google/src/serverGroup/configure/serverGroupCommandBuilder.service.js b/packages/google/src/serverGroup/configure/serverGroupCommandBuilder.service.js index 3ef9e98e9ec..7df1608c79b 100644 --- a/packages/google/src/serverGroup/configure/serverGroupCommandBuilder.service.js +++ b/packages/google/src/serverGroup/configure/serverGroupCommandBuilder.service.js @@ -295,6 +295,12 @@ angular } } + function populatePartnerMetadata(instanceTemplatePartnerMetadata, command) { + if (instanceTemplatePartnerMetadata) { + Object.assign(command.partnerMetadata, instanceTemplatePartnerMetadata); + } + } + function populateLabels(instanceTemplateLabels, command) { if (instanceTemplateLabels) { Object.assign(command.labels, instanceTemplateLabels); @@ -374,6 +380,7 @@ angular tags: [], labels: {}, resourceManagerTags: {}, + partnerMetadata: {}, enableSecureBoot: false, enableVtpm: false, enableIntegrityMonitoring: false, @@ -453,6 +460,7 @@ angular tags: [], labels: {}, resourceManagerTags: {}, + partnerMetadata: {}, availabilityZones: [], enableSecureBoot: serverGroup.enableSecureBoot, enableVtpm: serverGroup.enableVtpm, @@ -589,6 +597,9 @@ angular const resourceManagerTags = extendedCommand.resourceManagerTags; populateResourceManagerTags(resourceManagerTags, extendedCommand); + const partnerMetadata = extendedCommand.partnerMetadata; + populatePartnerMetadata(partnerMetadata, extendedCommand); + return extendedCommand; }); }); diff --git a/packages/google/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.directive.html b/packages/google/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.directive.html index 53c505a0200..6c0bd71d94b 100644 --- a/packages/google/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.directive.html +++ b/packages/google/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.directive.html @@ -94,6 +94,17 @@ +
+
+ Partner Metadata + +
+ +
Shielded VMs