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 @@ +
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: '', + 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'), + }); 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('Skip SpEL expression evaluation of the manifest artifact in this stage. Can be paired with the "Evaluate SpEL expressions in overrides at bake time" option in the Bake Manifest stage when baking a third-party manifest artifact with expressions not meant for Spinnaker to evaluate as SpEL.
', + 'kubernetes.manifest.skipSpecTemplateLabels': ` +Skip applying labels to a manifest's .spec.template.metadata.labels.
`, 'kubernetes.manifest.undoRollout.revisionsBack': `How many revisions to rollback from the current active revision. This is not a hard-coded revision to rollout.
For example: If you specify "1", and this stage executes, the prior revision will be active upon success.
diff --git a/packages/kubernetes/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx b/packages/kubernetes/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx index c818185097d..66a30239c8f 100644 --- a/packages/kubernetes/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx +++ b/packages/kubernetes/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx @@ -34,6 +34,7 @@ interface IDeployManifestStageConfigFormProps { interface IDeployManifestStageConfigFormState { rawManifest: string; overrideNamespace: boolean; + skipSpecTemplateLabels: boolean; } export class DeployManifestStageForm extends React.Component< @@ -55,6 +56,7 @@ export class DeployManifestStageForm extends React.Component< this.state = { rawManifest: !isEmpty(manifests) && isTextManifest ? yamlDocumentsToString(manifests) : '', overrideNamespace: get(stage, 'namespaceOverride', '') !== '', + skipSpecTemplateLabels: get(stage, 'skipSpecTemplateLabels', false), }; } @@ -141,6 +143,12 @@ export class DeployManifestStageForm extends React.Component< /> )} +