Skip to content

Commit

Permalink
feat(google): add support for partnerMetadata in GCE servergroup (#10150
Browse files Browse the repository at this point in the history
)

* feat(google): add support for partnerMetadata in GCE servergroup

* feat(google): fix prettier issues

* feat(google): render JSON object as its string representation

* feat(google): update tests with textarea

* feat(google): fix prettier issue

* feat(google): move display logic to controller

* feat(google): fix tests

* feat(google): expect JSON string in test

* feat(google): use formatValueForDisplay in ng-model

* feat(google): define json-text directive to handle view

* feat(google): add space to JSON format

* feat(google): remove hiddenKeys test
  • Loading branch information
edgarulg authored Oct 21, 2024
1 parent 14977d1 commit 6b004d3
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/forms/forms.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<form name="mapObjectEditor">
<div class="sm-label-left" ng-if="$ctrl.label">
<b>{{ $ctrl.label }}</b>
</div>
<input class="form-control input-sm" ng-model="$ctrl.model" ng-if="$ctrl.isParameterized" />
<table class="table table-condensed packed tags {{ $ctrl.tableClass }}" ng-if="!$ctrl.isParameterized">
<thead>
<tr ng-if="!$ctrl.labelsLeft">
<th ng-bind="$ctrl.keyLabel"></th>
<th ng-bind="$ctrl.valueLabel"></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="pair in $ctrl.backingModel" ng-if="!$ctrl.hiddenKeys.includes(pair.key)">
<td class="table-label" ng-if="$ctrl.labelsLeft">
<b>{{ $ctrl.keyLabel }}</b>
</td>
<td>
<input
class="form-control input input-sm"
type="text"
name="{{ $index }}"
ng-model="pair.key"
validate-unique="pair.checkUnique"
/>
<div class="error-message" ng-if="mapObjectEditor[$index].$error.validateUnique">Duplicate key</div>
</td>
<td class="table-label" ng-if="$ctrl.labelsLeft">
<b>{{ $ctrl.valueLabel }}</b>
</td>
<td>
<textarea json-text class="form-control input input-sm" ng-model="pair.value" rows="4"></textarea>
</td>
<td>
<div class="form-control-static">
<a href ng-click="$ctrl.removeField($index)">
<span class="glyphicon glyphicon-trash"></span>
<span class="sr-only">Remove field</span>
</a>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="{{ $ctrl.columnCount }}">
<button class="btn btn-block btn-sm add-new" ng-click="$ctrl.addField()">
<span class="glyphicon glyphicon-plus-sign"></span>
{{ $ctrl.addButtonLabel }}
</button>
</td>
</tr>
</tfoot>
</table>
</form>
113 changes: 113 additions & 0 deletions packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.js
Original file line number Diff line number Diff line change
@@ -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: '<?',
label: '@',
hiddenKeys: '<',
},
controller: [
'$scope',
function ($scope) {
this.backingModel = [];

const modelKeys = () => 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'),
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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('<map-object-editor model="model"></map-object-editor>')(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('<map-object-editor model="model"></map-object-editor>')(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('<map-object-editor model="model"></map-object-editor>')(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('<map-object-editor model="model"></map-object-editor>')(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('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();

$(dom.find('input')[1]).val('a').trigger('input');
scope.$digest();

expect(dom.find('.error-message').length).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -374,6 +380,7 @@ angular
tags: [],
labels: {},
resourceManagerTags: {},
partnerMetadata: {},
enableSecureBoot: false,
enableVtpm: false,
enableIntegrityMonitoring: false,
Expand Down Expand Up @@ -453,6 +460,7 @@ angular
tags: [],
labels: {},
resourceManagerTags: {},
partnerMetadata: {},
availabilityZones: [],
enableSecureBoot: serverGroup.enableSecureBoot,
enableVtpm: serverGroup.enableVtpm,
Expand Down Expand Up @@ -589,6 +597,9 @@ angular
const resourceManagerTags = extendedCommand.resourceManagerTags;
populateResourceManagerTags(resourceManagerTags, extendedCommand);

const partnerMetadata = extendedCommand.partnerMetadata;
populatePartnerMetadata(partnerMetadata, extendedCommand);

return extendedCommand;
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@
</div>
<map-editor model="vm.command.resourceManagerTags" add-button-label="Add New Tag" allow-empty="false"></map-editor>
</div>
<div class="form-group">
<div class="sm-label-left">
<b>Partner Metadata</b>
<help-field key="gce.serverGroup.partnerMetadata"></help-field>
</div>
<map-object-editor
model="vm.command.partnerMetadata"
add-button-label="Add New Metadata"
allow-empty="false"
></map-object-editor>
</div>
<div class="form-group">
<div class="sm-label-left">
Shielded VMs
Expand Down

0 comments on commit 6b004d3

Please sign in to comment.