Skip to content

Commit

Permalink
feat(core): render pipeline stages without UIs
Browse files Browse the repository at this point in the history
  • Loading branch information
danielpeach committed Apr 13, 2017
1 parent 90f01bc commit a2e784c
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 14 deletions.
3 changes: 3 additions & 0 deletions app/scripts/modules/core/core.module.js
Expand Up @@ -16,6 +16,7 @@ import {VERSION_CHECK_SERVICE} from './config/versionCheck.service';
import {CORE_WIDGETS_MODULE} from './widgets';
import {TRAVIS_STAGE_MODULE} from './pipeline/config/stages/travis/travisStage.module';
import {WEBHOOK_STAGE_MODULE} from './pipeline/config/stages/webhook/webhookStage.module';
import {UNMATCHED_STAGE_TYPE_STAGE} from './pipeline/config/stages/unmatchedStageTypeStage/unmatchedStageTypeStage';
import {SETTINGS} from 'core/config/settings';
import {INSIGHT_NGMODULE} from './insight/insight.module';

Expand Down Expand Up @@ -139,6 +140,8 @@ module.exports = angular

require('./task/task.module.js'),

UNMATCHED_STAGE_TYPE_STAGE,

require('./utils/utils.module.js'),

require('./whatsNew/whatsNew.directive.js'),
Expand Down
30 changes: 18 additions & 12 deletions app/scripts/modules/core/pipeline/config/pipelineConfigProvider.js
Expand Up @@ -124,20 +124,26 @@ module.exports = angular.module('spinnaker.core.pipeline.config.configProvider',
var matches = getStageTypes().filter((stageType) => {
return stageType.key === stage.type || stageType.provides === stage.type || stageType.alias === stage.type;
});
if (matches.length > 1) {
var provider = stage.cloudProvider || stage.cloudProviderType || 'aws';
var matchesForStageCloudProvider = matches.filter((stageType) => {
return stageType.cloudProvider === provider;
});

if (!matchesForStageCloudProvider.length) {
return matches.find((stageType) => {
return stageType.cloudProvider || stageType.cloudProviderType;
}) || null;
}
return matchesForStageCloudProvider[0];
switch (matches.length) {
case 0:
return getStageTypes().find(stage => stage.key === 'unmatched') || null;
case 1:
return matches[0];
default:
const provider = stage.cloudProvider || stage.cloudProviderType || 'aws';
const matchesForStageCloudProvider = matches.filter(stageType => {
return stageType.cloudProvider === provider;
});

if (!matchesForStageCloudProvider.length) {
return matches.find(stageType => {
return stageType.cloudProvider || stageType.cloudProviderType;
}) || null;
} else {
return matchesForStageCloudProvider[0];
}
}
return matches.length ? matches[0] : null;
}, (stage) => [stage ? stage.type : '', stage ? stage.cloudProvider || stage.cloudProviderType || 'aws' : ''].join(':'));

function getTriggerConfig(type) {
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/modules/core/pipeline/config/stages/stage.html
Expand Up @@ -2,7 +2,7 @@
<div class="row pipeline-stage-config-heading">
<div class="col-md-3">
<h4 ng-bind="stage.name || '[new stage]'"></h4>
<p class="small" ng-if="stage.type"><strong>Stage type:</strong> {{label}}<br/> {{description}}</p>
<p class="small" ng-if="stage.type && label"><strong>Stage type:</strong> {{label}}<br/> {{description}}</p>
<p class="small" ng-if="extendedDescription" ng-bind-html="extendedDescription"></p>
<p class="small" ng-if="!stage.type">No stage type selected</p>
</div>
Expand Down
@@ -0,0 +1,81 @@
import {mock} from 'angular';

import {UNMATCHED_STAGE_TYPE_STAGE_CTRL, UnmatchedStageTypeStageCtrl} from './unmatchedStageTypeStage.controller';
import {JsonUtilityService} from 'core/utils/json/json.utility.service';

describe('Controller: UnmatchedStageTypeStageCtrl', () => {
let ctrl: UnmatchedStageTypeStageCtrl;

beforeEach(
mock.module(
UNMATCHED_STAGE_TYPE_STAGE_CTRL,
)
);

beforeEach(() => {
mock.inject(($controller: ng.IControllerService, $rootScope: ng.IRootScopeService, _jsonUtilityService_: JsonUtilityService) => {
ctrl = $controller('UnmatchedStageTypeStageCtrl', {
$scope: $rootScope.$new(),
jsonUtilityService: _jsonUtilityService_
}) as UnmatchedStageTypeStageCtrl;
ctrl.$onInit();
});
});

describe('Stage validation', () => {
it('throws an error if the given JSON encoded string is not valid JSON', () => {
ctrl.stageJson = `{
"type": "upsertLoadBalancer",
"comma-dangle": true,
}`;
ctrl.updateStage();
expect(ctrl.errorMessage).toBeDefined();
});

it('throws an error if the given JSON encoded string does not include a `type` property.', () => {
ctrl.stageJson = '{}';
ctrl.updateStage();
expect(ctrl.errorMessage).toBeDefined();
});
});

describe('Stage key removal', () => {
it('omits stage properties from JSON encoded string', () => {
ctrl.$scope.stage = {
refId: '1',
requisiteStageRefIds: [],
failPipeline: true,
};
ctrl.setStageJson();
expect(ctrl.stageJson).toEqual('{}');
});

it('maintains the omitted stage properties when updating with new values from JSON encoded string', () => {
ctrl.$scope.stage = {
refId: '1',
requisiteStageRefIds: [],
failPipeline: true,
};
ctrl.stageJson = `{"type": "upsertLoadBalancer"}`;
ctrl.updateStage();
expect(ctrl.$scope.stage).toEqual({
refId: '1',
requisiteStageRefIds: [],
failPipeline: true,
type: 'upsertLoadBalancer',
});
});

it('overrides the omitted stage properties with properties from the JSON encoded string', () => {
ctrl.$scope.stage = {
failPipeline: true,
};
ctrl.stageJson = `{"failPipeline": false, "type": "upsertLoadBalancer"}`;
ctrl.updateStage();
expect(ctrl.$scope.stage).toEqual({
failPipeline: false,
type: 'upsertLoadBalancer',
});
});
});
});
@@ -0,0 +1,71 @@
import {module, IComponentController, IScope, isDefined} from 'angular';
import {cloneDeep, isEqual} from 'lodash';
import {JSON_UTILITY_SERVICE, JsonUtilityService} from 'core/utils/json/json.utility.service';
import {IStage} from 'core/domain/IStage';

export class UnmatchedStageTypeStageCtrl implements IComponentController {
public stageJson: string;
public errorMessage: string;
public textareaRows: number;
// These values are editable with standard UI controls.
private keysToHide = new Set<string>(['refId', 'requisiteStageRefIds', 'failPipeline', 'continuePipeline',
'completeOtherBranchesThenFail', 'restrictExecutionDuringTimeWindow',
'restrictedExecutionWindow', 'stageEnabled', 'sendNotifications',
'notifications', 'comments', 'name']);

static get $inject() { return ['$scope', 'jsonUtilityService']; }

constructor(public $scope: IScope, private jsonUtilityService: JsonUtilityService) { }

public $onInit(): void {
this.stageJson = this.jsonUtilityService.makeSortedStringFromObject(this.makeCleanStageCopy(this.$scope.stage || {}));
this.textareaRows = this.stageJson.split('\n').length;
}

public updateStage(): void {
let parsedStage: IStage;
this.errorMessage = null;

try {
parsedStage = JSON.parse(this.stageJson);
} catch (e) {
this.errorMessage = e.message;
}
if (parsedStage && !parsedStage.type) {
this.errorMessage = 'Cannot delete property <em>type</em>.';
}

if (!this.errorMessage) {
Object.keys(this.$scope.stage).forEach(key => {
if (!this.keysToHide.has(key)) {
delete this.$scope.stage[key];
}
});
Object.assign(this.$scope.stage, parsedStage);
this.setStageJson();
}
}

public setStageJson(): void {
const stageCopy = this.makeCleanStageCopy(this.$scope.stage);
// If there are no property differences between the JSON string and the stage object, don't bother updating -
// we might end up cutting out whitespace unexpectedly.
if (!isEqual(stageCopy, JSON.parse(this.stageJson || '{}'))) {
this.stageJson = this.jsonUtilityService.makeStringFromObject(stageCopy);
}
}

private makeCleanStageCopy(stage: IStage): IStage {
const stageCopy = cloneDeep(stage);
this.keysToHide.forEach(key => {
if (isDefined(stageCopy[key])) {
delete stageCopy[key];
}
});
return stageCopy;
}
}

export const UNMATCHED_STAGE_TYPE_STAGE_CTRL = 'spinnaker.core.pipeline.stage.unmatchedStageTypeStage.controller';
module(UNMATCHED_STAGE_TYPE_STAGE_CTRL, [JSON_UTILITY_SERVICE])
.controller('UnmatchedStageTypeStageCtrl', UnmatchedStageTypeStageCtrl);
@@ -0,0 +1,20 @@
<div ng-controller="UnmatchedStageTypeStageCtrl as $ctrl">
<form name="form" class="form-horizontal flex-fill">
<div class="flex-fill">
<textarea class="code form-control flex-fill"
ng-model="$ctrl.stageJson"
spellcheck="false"
ng-blur="$ctrl.updateStage()"
ng-change="$ctrl.updateStage()"
ng-model-options="{'debounce': 500}"
rows="{{::$ctrl.textareaRows}}"></textarea>
</div>
</form>

<div class="form-group row" style="margin-top: 10px;">
<div class="col-md-9 col-md-offset-3 error-message slide-in" ng-if="$ctrl.errorMessage">
Error: <span ng-bind-html="$ctrl.errorMessage"></span>
</div>
</div>
</div>

@@ -0,0 +1,15 @@
import {module} from 'angular';
import {UNMATCHED_STAGE_TYPE_STAGE_CTRL} from './unmatchedStageTypeStage.controller';

export const UNMATCHED_STAGE_TYPE_STAGE = 'spinnaker.core.pipeline.stage.unmatchedStageType';
module(UNMATCHED_STAGE_TYPE_STAGE, [
require('core/pipeline/config/pipelineConfigProvider.js'),
UNMATCHED_STAGE_TYPE_STAGE_CTRL,
]).config((pipelineConfigProvider: any) => {
pipelineConfigProvider.registerStage({
key: 'unmatched',
synthetic: true,
templateUrl: require('./unmatchedStageTypeStage.html'),
});
});

6 changes: 5 additions & 1 deletion app/scripts/modules/core/utils/json/json.utility.service.ts
Expand Up @@ -61,7 +61,11 @@ export class JsonUtilityService {
}

public makeSortedStringFromObject(obj: any): string {
return JSON.stringify(this.sortObject(obj), null, 2);
return this.makeStringFromObject(this.sortObject(obj));
}

public makeStringFromObject(obj: any): string {
return JSON.stringify(obj, null, 2);
}

public diff(left: any, right: any, sortKeys = false): IJsonDiff {
Expand Down

0 comments on commit a2e784c

Please sign in to comment.