diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/triggers.html b/app/scripts/modules/core/src/pipeline/config/triggers/triggers.html
index 1e2f420075a..af246f2e87e 100644
--- a/app/scripts/modules/core/src/pipeline/config/triggers/triggers.html
+++ b/app/scripts/modules/core/src/pipeline/config/triggers/triggers.html
@@ -23,7 +23,7 @@
label="Expected Artifacts"
badge="pipeline.expectedArtifacts.length"
no-wrapper="true"
- visible="triggersCtrl.checkFeatureFlag('artifacts')"
+ visible="!triggersCtrl.checkFeatureFlag('artifactsRewrite') && triggersCtrl.checkFeatureFlag('artifacts')"
>
diff --git a/app/scripts/modules/core/src/task/monitor/TaskMonitor.ts b/app/scripts/modules/core/src/task/monitor/TaskMonitor.ts
index 64ff5764edb..a04b0fb6d18 100644
--- a/app/scripts/modules/core/src/task/monitor/TaskMonitor.ts
+++ b/app/scripts/modules/core/src/task/monitor/TaskMonitor.ts
@@ -56,7 +56,9 @@ export class TaskMonitor {
this.monitorInterval = config.monitorInterval || 1000;
this.submitMethod = config.submitMethod;
- this.modalInstance.result.then(() => this.onModalClose(), () => this.onModalClose());
+ if (this.modalInstance) {
+ this.modalInstance.result.then(() => this.onModalClose(), () => this.onModalClose());
+ }
}
public onModalClose(): void {
diff --git a/app/scripts/modules/core/src/utils/clipboard/CopyToClipboard.tsx b/app/scripts/modules/core/src/utils/clipboard/CopyToClipboard.tsx
index f0e643445a6..fb6678ecb18 100644
--- a/app/scripts/modules/core/src/utils/clipboard/CopyToClipboard.tsx
+++ b/app/scripts/modules/core/src/utils/clipboard/CopyToClipboard.tsx
@@ -9,6 +9,7 @@ export interface ICopyToClipboardProps {
displayText?: boolean;
text: string;
toolTip: string;
+ className?: string;
}
interface ICopyToClipboardState {
diff --git a/app/scripts/modules/core/src/utils/index.ts b/app/scripts/modules/core/src/utils/index.ts
index a179f0b800b..cdb72029c56 100644
--- a/app/scripts/modules/core/src/utils/index.ts
+++ b/app/scripts/modules/core/src/utils/index.ts
@@ -9,3 +9,4 @@ export * from './scrollTo/scrollTo.service';
export * from './timeFormatters';
export * from './uuid.service';
export * from './workerPool';
+export * from './renderIfFeature.component';
diff --git a/app/scripts/modules/core/src/widgets/index.ts b/app/scripts/modules/core/src/widgets/index.ts
index 6215a573fd9..826440544d8 100644
--- a/app/scripts/modules/core/src/widgets/index.ts
+++ b/app/scripts/modules/core/src/widgets/index.ts
@@ -8,3 +8,4 @@ export * from './spinners/Spinner';
export * from './ScopeClusterSelector';
export * from './tags';
export * from './spelText/SpelNumberInput';
+export * from './spelText/SpelText';
diff --git a/app/scripts/modules/core/src/widgets/spelText/SpelAutocompleteService.ts b/app/scripts/modules/core/src/widgets/spelText/SpelAutocompleteService.ts
new file mode 100644
index 00000000000..eec0c1e24f9
--- /dev/null
+++ b/app/scripts/modules/core/src/widgets/spelText/SpelAutocompleteService.ts
@@ -0,0 +1,420 @@
+import { ExecutionService } from '../../pipeline/service/execution.service';
+import { IPipeline, IExecution, IStage } from '../../domain';
+import { JsonListBuilder } from './JsonListBuilder';
+import { IPromise, IQService } from 'angular';
+
+interface IBracket {
+ open: string;
+ close: string;
+}
+
+interface ITextcompleteConfigElement {
+ id: string;
+
+ [k: string]: any;
+}
+
+interface IHelperParam {
+ name: string;
+ type: string;
+}
+
+interface IExecutionCache {
+ [k: string]: IExecution;
+}
+
+export class SpelAutocompleteService {
+ private executionCache: IExecutionCache = {};
+
+ private brackets: IBracket[] = [{ open: '(', close: ')' }, { open: '[', close: ']' }];
+
+ private quotes: IBracket[] = [{ open: "'", close: "'" }, { open: '"', close: '"' }];
+
+ private helperFunctions = [
+ 'alphanumerical',
+ 'readJson',
+ 'fromUrl',
+ 'propertiesFromUrl',
+ 'jsonFromUrl',
+ 'judgment',
+ 'stage',
+ 'toBoolean',
+ 'toFloat',
+ 'toInt',
+ 'toJson',
+ 'toBase64',
+ 'fromBase64',
+ ];
+
+ private helperParams = [
+ 'execution',
+ 'parameters',
+ 'trigger',
+ 'scmInfo',
+ 'scmInfo.sha1',
+ 'scmInfo.branch',
+ 'deployedServerGroups',
+ ];
+
+ private codedHelperParams: IHelperParam[] = this.helperParams.map((param: string) => {
+ return { name: param, type: 'param' };
+ });
+
+ public textcompleteConfig: ITextcompleteConfigElement[] = [
+ {
+ id: 'SpEL wrapper',
+ match: /\$(\w*)$/,
+ search: function(_: any, callback: (value: string[]) => void) {
+ callback(['${...}']);
+ },
+ index: 1,
+ replace: function replace() {
+ return ['${ ', ' }'];
+ },
+ },
+ {
+ id: 'match quotes',
+ match: /(["'])(\w*)$/,
+ index: 1,
+ search: (term: string, callback: (value: IBracket[]) => void) => {
+ callback(this.quotes.filter((bracket: IBracket) => bracket.open.indexOf(term) === 0));
+ },
+ template: function() {
+ return `'...'`;
+ },
+ replace: function replace() {
+ return [`'`, `'`];
+ },
+ },
+ {
+ id: 'match brackets',
+ match: /([\[('])(\w*)$/,
+ index: 1,
+ search: (term: string, callback: (value: IBracket[]) => void) => {
+ callback(this.brackets.filter((bracket: IBracket) => bracket.open.indexOf(term) === 0));
+ },
+ template: (value: IBracket) => {
+ return `${value.open}...${value.close}`;
+ },
+ replace: (bracket: IBracket) => {
+ return [`${bracket.open} `, ` ${bracket.close}`];
+ },
+ },
+ ];
+
+ constructor(private $q: IQService, private executionService: ExecutionService) {
+ 'ngInject';
+ }
+
+ private paramInList(checkParam: { name: string }) {
+ return (testParam: { name: string }) => checkParam.name === testParam.name;
+ }
+
+ private addToTextcompleteConfig(
+ configList: ITextcompleteConfigElement[],
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ const textcompleteConfigCopy = textcompleteConfig.slice(0);
+
+ for (const newConfig of configList) {
+ if (textcompleteConfig.filter(config => config.id === newConfig.id).length === 0) {
+ textcompleteConfigCopy.push(newConfig);
+ }
+ }
+
+ return textcompleteConfigCopy;
+ }
+
+ private listSearchFn(list: any) {
+ return (term: any, callback: any) => {
+ callback(
+ list.filter((item: any) => {
+ if (item.leaf.includes(term)) {
+ return item;
+ }
+ }),
+ );
+ };
+ }
+
+ private leafTemplateFn(stage: any) {
+ return `${
+ stage.leaf
+ } ${stage.value.length > 90 ? `${stage.value.slice(0, 90)}...` : stage.value} `;
+ }
+
+ private addExecutionForAutocomplete(execution: IExecution, textcompleteConfig: ITextcompleteConfigElement[]) {
+ if (execution) {
+ const executionList = JsonListBuilder.convertJsonKeysToBracketedList(execution, ['task']);
+ const configList = [
+ {
+ id: `execution: ${execution.id}`,
+ match: /execution(\w*|\s*)$/,
+ index: 1,
+ search: this.listSearchFn(executionList),
+ template: this.leafTemplateFn,
+ replace: (value: any) => {
+ return `execution${value.leaf}`;
+ },
+ },
+ ];
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+ return textcompleteConfig;
+ }
+
+ private addDeloyedServerGroupsForAutoComplete(
+ execution: IExecution,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (execution && execution.context && execution.context.deploymentDetails) {
+ const deployList = JsonListBuilder.convertJsonKeysToBracketedList(execution.context.deploymentDetails);
+ const configList = [
+ {
+ id: `deploymentServerGroups: ${execution.id}`,
+ match: /deployedServerGroups(\w*|\s*)$/,
+ index: 1,
+ search: this.listSearchFn(deployList),
+ template: this.leafTemplateFn,
+ replace: (value: any) => {
+ return `deployedServerGroups${value.leaf}`;
+ },
+ },
+ ];
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+ return textcompleteConfig;
+ }
+
+ private addStageDataForAutocomplete(
+ pipeline: IPipeline | IExecution,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.stages) {
+ const configList = (pipeline.stages as IStage[]).map(stage => {
+ const stageList = JsonListBuilder.convertJsonKeysToBracketedList(stage, ['task']);
+
+ return {
+ id: `stage config for ${stage.name}`,
+ match: new RegExp(`#stage\\(\\s*'${JsonListBuilder.escapeForRegEx(stage.name)}'\\s*\\)(.*)$`),
+ index: 1,
+ search: this.listSearchFn(stageList),
+ template: this.leafTemplateFn,
+ replace: (param: any) => {
+ return `#stage('${stage.name}')${param.leaf}`;
+ },
+ };
+ });
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+
+ return textcompleteConfig;
+ }
+
+ private addManualJudgementConfigForAutocomplete(
+ pipeline: IPipeline | IExecution,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.stages) {
+ const manualJudgementStageList = pipeline.stages.filter(stage => stage.type === 'manualJudgment');
+
+ const configList = manualJudgementStageList.map(stage => {
+ const stageList = JsonListBuilder.convertJsonKeysToBracketedList(stage);
+
+ return {
+ id: `judgement config for ${stage.name}`,
+ match: new RegExp(`#judgment\\(\\s*'\\s*${JsonListBuilder.escapeForRegEx(stage.name)}'\\s*\\)(.*)$`),
+ index: 1,
+ search: this.listSearchFn(stageList),
+ template: this.leafTemplateFn,
+ replace: (param: any) => {
+ return `#judgement('${stage.name}')${param.leaf}`;
+ },
+ };
+ });
+
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+
+ return textcompleteConfig;
+ }
+
+ private addTriggerConfigForAutocomplete(
+ pipeline: IExecution,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.trigger) {
+ const triggerAsList = [pipeline.trigger];
+ const configList = triggerAsList.map(trigger => {
+ const triggerInfoList = JsonListBuilder.convertJsonKeysToBracketedList(trigger);
+ return {
+ id: `trigger config: ${trigger.type}`,
+ match: /trigger(\w*|\s*)$/,
+ index: 1,
+ search: this.listSearchFn(triggerInfoList),
+ template: this.leafTemplateFn,
+ replace: (value: any) => {
+ return `trigger${value.leaf}`;
+ },
+ };
+ });
+
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+
+ return textcompleteConfig;
+ }
+
+ private addParameterConfigForAutocomplete(
+ pipeline: IPipeline,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.parameterConfig) {
+ const paramsAsList = [pipeline.parameterConfig];
+ const configList = paramsAsList.map(params => {
+ const paramsInfoList = JsonListBuilder.convertJsonKeysToBracketedList(params);
+ return {
+ id: `parameter config: ${Object.keys(params).join(',')}`,
+ match: /parameters(\w*|\s*)$/,
+ index: 1,
+ search: this.listSearchFn(paramsInfoList),
+ template: this.leafTemplateFn,
+ replace: (value: any) => {
+ return `parameters${value.leaf}`;
+ },
+ };
+ });
+
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+
+ return textcompleteConfig;
+ }
+
+ private addStageNamesToCodeHelperList(
+ pipeline: IExecution & IPipeline,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.stages) {
+ let codedHelperParamsCopy = this.codedHelperParams.slice(0);
+
+ const pipelineHasParameters = pipeline.parameterConfig && pipeline.parameterConfig.length;
+ codedHelperParamsCopy = pipelineHasParameters
+ ? codedHelperParamsCopy
+ : codedHelperParamsCopy.filter(param => param.name !== 'parameters');
+
+ const hasJenkinsTriggerOrStage =
+ (pipeline.trigger && pipeline.trigger.type === 'jenkins') ||
+ pipeline.stages.some(stage => stage.type === 'jenkins');
+ codedHelperParamsCopy = hasJenkinsTriggerOrStage
+ ? codedHelperParamsCopy
+ : codedHelperParamsCopy.filter(param => !param.name.includes('scmInfo'));
+
+ pipeline.stages.forEach(stage => {
+ const newParam = { name: stage.name, type: stage.type };
+ if (codedHelperParamsCopy.filter(this.paramInList(newParam)).length === 0) {
+ codedHelperParamsCopy.push({ name: stage.name, type: 'stage' });
+ }
+ });
+
+ const configList = [
+ {
+ id: 'params',
+ match: /(\s*|\w*)\?(\s*|\w*|')$/,
+ index: 2,
+ search: (term: string, callback: (value: IHelperParam[]) => void) => {
+ callback(
+ codedHelperParamsCopy.filter((param: any) => param.name.includes(term) || param.type.includes(term)),
+ );
+ },
+ template: (value: IHelperParam) => ` ${value.name}`,
+ replace: (param: IHelperParam) => `${param.name}`,
+ },
+ ];
+
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+ return textcompleteConfig;
+ }
+
+ private addHelperFunctionsBasedOnStages(
+ pipeline: IPipeline | IExecution,
+ textcompleteConfig: ITextcompleteConfigElement[],
+ ): ITextcompleteConfigElement[] {
+ if (pipeline && pipeline.stages) {
+ let helperFunctionsCopy = this.helperFunctions.slice(0);
+ const hasManualJudmentStage = pipeline.stages.some(stage => stage.type === 'manualJudgment');
+ if (!hasManualJudmentStage) {
+ helperFunctionsCopy = this.helperFunctions.filter(fnName => fnName !== 'judgment');
+ }
+
+ const configList = [
+ {
+ id: 'helper functions',
+ match: /#(\w*)$/,
+ index: 1,
+ search: (term: string, callback: (value: string[]) => void) => {
+ callback(helperFunctionsCopy.filter((helper: string) => helper.indexOf(term) === 0));
+ },
+ template: (value: string) => ` #${value}`,
+ replace: (helper: string) => (helper === 'toJson' ? [`#${helper}(`, ')'] : [`#${helper}( '`, `' )`]),
+ },
+ ];
+
+ return this.addToTextcompleteConfig(configList, textcompleteConfig);
+ }
+ return textcompleteConfig;
+ }
+
+ private getLastExecutionByPipelineConfig(pipelineConfig: IPipeline): IPromise {
+ if (this.executionCache[pipelineConfig.id]) {
+ return this.$q.when(this.executionCache[pipelineConfig.id]);
+ } else {
+ return this.executionService
+ .getLastExecutionForApplicationByConfigId(pipelineConfig.application, pipelineConfig.id)
+ .then(execution => {
+ if (execution) {
+ this.executionCache[pipelineConfig.id] = execution;
+ return execution;
+ } else {
+ return null;
+ }
+ });
+ }
+ }
+
+ public addPipelineInfo(pipelineConfig: IPipeline) {
+ if (pipelineConfig && pipelineConfig.id) {
+ return this.getLastExecutionByPipelineConfig(pipelineConfig)
+ .then(lastExecution => lastExecution || pipelineConfig)
+ .then((pipeline: IPipeline & IExecution) =>
+ this.addStageNamesToCodeHelperList(
+ pipeline,
+ this.addStageDataForAutocomplete(
+ pipeline,
+ this.addManualJudgementConfigForAutocomplete(
+ pipeline,
+ this.addTriggerConfigForAutocomplete(
+ pipeline,
+ this.addParameterConfigForAutocomplete(
+ // TODO does this not work on stages?
+ pipeline,
+ this.addHelperFunctionsBasedOnStages(
+ pipeline,
+ this.addExecutionForAutocomplete(
+ pipeline,
+ this.addDeloyedServerGroupsForAutoComplete(pipeline, this.textcompleteConfig.slice(0)),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ } else {
+ return this.$q.when(this.textcompleteConfig);
+ }
+ }
+}
diff --git a/app/scripts/modules/core/src/widgets/spelText/SpelText.tsx b/app/scripts/modules/core/src/widgets/spelText/SpelText.tsx
new file mode 100644
index 00000000000..c862a0ea7fb
--- /dev/null
+++ b/app/scripts/modules/core/src/widgets/spelText/SpelText.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react';
+
+import 'jquery-textcomplete';
+import './spel.less';
+
+import * as classNames from 'classnames';
+import * as $ from 'jquery';
+import { $q, $http, $timeout } from 'ngimport';
+import { SpelAutocompleteService } from './SpelAutocompleteService';
+import { ExecutionService } from '../../pipeline/service/execution.service';
+import { StateService } from '@uirouter/core';
+import { IPipeline } from '../../domain';
+
+export interface ISpelTextProps {
+ placeholder: string;
+ value?: string;
+ onChange: (value: string) => void;
+ pipeline: IPipeline;
+ docLink: boolean;
+}
+
+export interface ISpelTextState {
+ textcompleteConfig: any[];
+}
+
+export class SpelText extends React.Component {
+ private autocompleteService: SpelAutocompleteService;
+ private spelInputRef: any;
+
+ constructor(props: ISpelTextProps) {
+ super(props);
+ this.state = { textcompleteConfig: [] };
+ this.autocompleteService = new SpelAutocompleteService(
+ $q,
+ new ExecutionService($http, $q, {} as StateService, $timeout),
+ );
+ this.autocompleteService.addPipelineInfo(this.props.pipeline).then(textcompleteConfig => {
+ this.setState({ textcompleteConfig: textcompleteConfig });
+ });
+ this.spelInputRef = React.createRef();
+ }
+
+ public componentDidMount(): void {
+ this.renderSuggestions();
+ }
+
+ public componentDidUpdate() {
+ this.renderSuggestions();
+ }
+
+ private renderSuggestions() {
+ const input = $(this.spelInputRef.current);
+ input.attr('contenteditable', 'true');
+ input.textcomplete(this.state.textcompleteConfig, {
+ maxCount: 1000,
+ zIndex: 9000,
+ dropdownClassName: 'dropdown-menu textcomplete-dropdown spel-dropdown',
+ });
+ }
+
+ public render() {
+ return (
+ this.props.onChange(e.target.value)}
+ ref={this.spelInputRef}
+ />
+ );
+ }
+}
diff --git a/app/scripts/modules/core/src/widgets/spelText/spelAutocomplete.service.js b/app/scripts/modules/core/src/widgets/spelText/spelAutocomplete.service.js
index 00a8dd04a96..42ec5bee8c4 100644
--- a/app/scripts/modules/core/src/widgets/spelText/spelAutocomplete.service.js
+++ b/app/scripts/modules/core/src/widgets/spelText/spelAutocomplete.service.js
@@ -1,416 +1,16 @@
'use strict';
import { EXECUTION_SERVICE } from 'core/pipeline/service/execution.service';
-import { JsonListBuilder } from './JsonListBuilder';
+import { SpelAutocompleteService } from './SpelAutocompleteService';
const angular = require('angular');
module.exports = angular
.module('spinnaker.core.widget.spelAutocomplete', [EXECUTION_SERVICE])
- .factory('spelAutocomplete', [
- '$q',
- 'executionService',
- function($q, executionService) {
- let brackets = [{ open: '(', close: ')' }, { open: '[', close: ']' }];
- let quotes = [{ open: "'", close: "'" }, { open: '"', close: '"' }];
- let helperFunctions = [
- 'alphanumerical',
- 'readJson',
- 'fromUrl',
- 'propertiesFromUrl',
- 'jsonFromUrl',
- 'judgment',
- 'stage',
- 'toBoolean',
- 'toFloat',
- 'toInt',
- 'toJson',
- 'toBase64',
- 'fromBase64',
- ];
- let helperParams = [
- 'execution',
- 'parameters',
- 'trigger',
- 'scmInfo',
- 'scmInfo.sha1',
- 'scmInfo.branch',
- 'deployedServerGroups',
- ];
- let codedHelperParams = helperParams.map(param => {
- return { name: param, type: 'param' };
- });
-
- let textcompleteConfig = [
- {
- id: 'SpEL wrapper',
- match: /\$(\w*)$/,
- search: function(item, callback) {
- callback(['${...}']);
- },
- index: 1,
- replace: function replace() {
- return ['${ ', ' }'];
- },
- },
- {
- id: 'match quotes',
- match: /("|')(\w*)$/,
- index: 1,
- search: function(term, callback) {
- let found = quotes.filter(quote => {
- if (quote.open.indexOf(term) === 0) {
- return quote;
- }
- });
- callback(found);
- },
- template: function() {
- return `'...'`;
- },
- replace: function replace() {
- return [`'`, `'`];
- },
- },
- {
- id: 'match brackets',
- match: /(\[|\(|')(\w*)$/,
- index: 1,
- search: function(term, callback) {
- let found = brackets.filter(bracket => {
- if (bracket.open.indexOf(term) === 0) {
- return bracket;
- }
- });
- callback(found);
- },
- template: function(value) {
- return `${value.open}...${value.close}`;
- },
- replace: function replace(bracket) {
- return [`${bracket.open} `, ` ${bracket.close}`];
- },
- },
- ];
-
- let paramInList = checkParam => {
- return testParam => checkParam.name === testParam.name;
- };
-
- let addToTextcompleteConfig = (configList = [], textcompleteConfig) => {
- let textcompleteConfigCopy = textcompleteConfig.slice(0);
-
- configList.forEach(newConfig => {
- if (textcompleteConfig.filter(config => config.id === newConfig.id).length === 0) {
- return textcompleteConfigCopy.push(newConfig);
- }
- });
-
- return textcompleteConfigCopy;
- };
-
- const listSearchFn = list => {
- return (term, callback) => {
- callback(
- list.filter(item => {
- if (item.leaf.includes(term)) {
- return item;
- }
- }),
- );
- };
- };
-
- const leafTemplateFn = stage => {
- return `${
- stage.leaf
- } ${stage.value.length > 90 ? `${stage.value.slice(0, 90)}...` : stage.value} `;
- };
-
- let addExecutionForAutocomplete = (pipeline, textcompleteConfig) => {
- if (pipeline) {
- let executionList = JsonListBuilder.convertJsonKeysToBracketedList(pipeline, ['task']);
-
- let configList = [
- {
- id: `execution: ${pipeline.id}`,
- match: /execution(\w*|\s*)$/,
- index: 1,
- search: listSearchFn(executionList),
- template: leafTemplateFn,
- replace: value => {
- return `execution${value.leaf}`;
- },
- },
- ];
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
- return textcompleteConfig;
- };
-
- let addDeloyedServerGroupsForAutoComplete = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.context && pipeline.context.deploymentDetails) {
- let deployList = JsonListBuilder.convertJsonKeysToBracketedList(pipeline.context.deploymentDetails);
-
- let configList = [
- {
- id: `deploymentServerGroups: ${pipeline.id}`,
- match: /deployedServerGroups(\w*|\s*)$/,
- index: 1,
- search: listSearchFn(deployList),
- template: leafTemplateFn,
- replace: value => {
- return `deployedServerGroups${value.leaf}`;
- },
- },
- ];
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
- return textcompleteConfig;
- };
-
- let addStageDataForAutocomplete = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.stages) {
- let configList = pipeline.stages.map(stage => {
- let stageList = JsonListBuilder.convertJsonKeysToBracketedList(stage, ['task']);
-
- return {
- id: `stage config for ${stage.name}`,
- match: new RegExp(`#stage\\(\\s*'${JsonListBuilder.escapeForRegEx(stage.name)}'\\s*\\)(.*)$`),
- index: 1,
- search: listSearchFn(stageList),
- template: leafTemplateFn,
- replace: param => {
- return `#stage('${stage.name}')${param.leaf}`;
- },
- };
- });
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
-
- return textcompleteConfig;
- };
-
- let addManualJudgementConfigForAutocomplete = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.stages) {
- let manualJudgementStageList = pipeline.stages.filter(stage => stage.type === 'manualJudgment');
-
- let configList = manualJudgementStageList.map(stage => {
- let stageList = JsonListBuilder.convertJsonKeysToBracketedList(stage);
-
- return {
- id: `judgement config for ${stage.name}`,
- match: new RegExp(`#judgment\\(\\s*'\\s*${JsonListBuilder.escapeForRegEx(stage.name)}'\\s*\\)(.*)$`),
- index: 1,
- search: listSearchFn(stageList),
- template: leafTemplateFn,
- replace: param => {
- return `#judgement('${stage.name}')${param.leaf}`;
- },
- };
- });
-
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
-
- return textcompleteConfig;
- };
-
- let addTriggerConfigForAutocomplete = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.trigger) {
- let triggerAsList = [pipeline.trigger];
- let configList = triggerAsList.map(trigger => {
- let triggerInfoList = JsonListBuilder.convertJsonKeysToBracketedList(trigger);
- return {
- id: `trigger config: ${trigger.type}`,
- match: /trigger(\w*|\s*)$/,
- index: 1,
- search: listSearchFn(triggerInfoList),
- template: leafTemplateFn,
- replace: function replace(value) {
- return `trigger${value.leaf}`;
- },
- };
- });
-
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
-
- return textcompleteConfig;
- };
-
- let addParameterConfigForAutocomplete = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.parameterConfig) {
- let paramsAsList = [pipeline.parameterConfig];
- let configList = paramsAsList.map(params => {
- let paramsInfoList = JsonListBuilder.convertJsonKeysToBracketedList(params);
- return {
- id: `parameter config: ${Object.keys(params).join(',')}`,
- match: /parameters(\w*|\s*)$/,
- index: 1,
- search: listSearchFn(paramsInfoList),
- template: leafTemplateFn,
- replace: function replace(value) {
- return `parameters${value.leaf}`;
- },
- };
- });
-
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
-
- return textcompleteConfig;
- };
-
- let addStageNamesToCodeHelperList = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.stages) {
- let codedHelperParamsCopy = codedHelperParams.slice(0);
-
- let pipelineHasParameters = pipeline.parameterConfig && pipeline.parameterConfig.length;
- codedHelperParamsCopy = pipelineHasParameters
- ? codedHelperParamsCopy
- : codedHelperParamsCopy.filter(param => param.name !== 'parameters');
- let trigger = pipeline.trigger || {};
- let hasJenkinsTriggerOrStage =
- trigger.type === 'jenkins' || pipeline.stages.some(stage => stage.type === 'jenkins');
- codedHelperParamsCopy = hasJenkinsTriggerOrStage
- ? codedHelperParamsCopy
- : codedHelperParamsCopy.filter(param => !param.name.includes('scmInfo'));
-
- pipeline.stages.forEach(stage => {
- let newParam = { name: stage.name, type: stage.type };
- if (codedHelperParamsCopy.filter(paramInList(newParam)).length === 0) {
- codedHelperParamsCopy.push({ name: stage.name, type: 'stage' });
- }
- });
-
- let configList = [
- {
- id: 'params',
- match: /(\s*|\w*)\?(\s*|\w*|')$/,
- index: 2,
- search: function(term, callback) {
- callback(
- codedHelperParamsCopy.filter(param => {
- if (param.name.includes(term) || param.type.includes(term)) {
- return param;
- }
- }),
- );
- },
- template: function(value) {
- return ` ${value.name}`;
- },
- replace: function replace(param) {
- return `${param.name}`;
- },
- },
- ];
-
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
- return textcompleteConfig;
- };
-
- let addHelperFunctionsBasedOnStages = (pipeline, textcompleteConfig) => {
- if (pipeline && pipeline.stages) {
- let helperFunctionsCopy = helperFunctions.slice(0);
- let hasManualJudmentStage = pipeline.stages.some(stage => stage.type === 'manualJudgment');
- if (!hasManualJudmentStage) {
- helperFunctionsCopy = helperFunctions.filter(fnName => fnName !== 'judgment');
- }
-
- let configList = [
- {
- id: 'helper functions',
- match: /#(\w*)$/,
- index: 1,
- search: function(term, callback) {
- callback(
- helperFunctionsCopy.filter(helper => {
- if (helper.indexOf(term) === 0) {
- return helper;
- }
- }),
- );
- },
- template: value => {
- return ` #${value}`;
- },
-
- replace: function replace(helper) {
- if (helper === 'toJson') {
- return [`#${helper}(`, ')'];
- }
- return [`#${helper}( '`, `' )`];
- },
- },
- ];
-
- return addToTextcompleteConfig(configList, textcompleteConfig);
- }
- return textcompleteConfig;
- };
-
- let executionCache = {};
-
- let getLastExecutionByPipelineConfig = pipelineConfig => {
- if (executionCache[pipelineConfig.id]) {
- return $q.when(executionCache[pipelineConfig.id]);
- } else {
- return executionService
- .getLastExecutionForApplicationByConfigId(pipelineConfig.application, pipelineConfig.id)
- .then(execution => {
- if (execution) {
- executionCache[pipelineConfig.id] = execution;
- return execution;
- } else {
- return null;
- }
- });
- }
- };
-
- let addPipelineInfo = pipelineConfig => {
- if (pipelineConfig && pipelineConfig.id) {
- return getLastExecutionByPipelineConfig(pipelineConfig)
- .then(lastExecution => {
- return lastExecution || pipelineConfig;
- })
- .then(pipeline => {
- return addStageNamesToCodeHelperList(
- pipeline,
- addStageDataForAutocomplete(
- pipeline,
- addManualJudgementConfigForAutocomplete(
- pipeline,
- addTriggerConfigForAutocomplete(
- pipeline,
- addParameterConfigForAutocomplete(
- pipeline,
- addHelperFunctionsBasedOnStages(
- pipeline,
- addExecutionForAutocomplete(
- pipeline,
- addDeloyedServerGroupsForAutoComplete(pipeline, textcompleteConfig.slice(0)),
- ),
- ),
- ),
- ),
- ),
- ),
- );
- });
- } else {
- return $q.when(textcompleteConfig);
- }
- };
-
- return {
- textcompleteConfig: textcompleteConfig,
- addPipelineInfo: addPipelineInfo,
- };
- },
- ]);
+ .factory('spelAutocomplete', ($q, executionService) => {
+ const autocomplete = new SpelAutocompleteService($q, executionService);
+ return {
+ textcompleteConfig: autocomplete.textcompleteConfig,
+ addPipelineInfo: pipelineConfig => autocomplete.addPipelineInfo(pipelineConfig),
+ };
+ });
diff --git a/app/scripts/modules/core/src/widgets/spelText/spelText.decorator.js b/app/scripts/modules/core/src/widgets/spelText/spelText.decorator.js
index cf0650a5fc4..97ab808fbdb 100644
--- a/app/scripts/modules/core/src/widgets/spelText/spelText.decorator.js
+++ b/app/scripts/modules/core/src/widgets/spelText/spelText.decorator.js
@@ -32,6 +32,10 @@ function decorateFn($delegate, spelAutocomplete) {
});
function listener(evt) {
+ if ($(evt.target).hasClass('no-doc-link')) {
+ return;
+ }
+
let hasSpelPrefix = evt.target.value.includes('$');
let parent = el.parent();
let hasLink = parent && parent.nextAll && parent.nextAll('.spelLink');
diff --git a/app/scripts/modules/kubernetes/src/v2/kubernetes.v2.module.ts b/app/scripts/modules/kubernetes/src/v2/kubernetes.v2.module.ts
index fec52722a66..0decd0c4b02 100644
--- a/app/scripts/modules/kubernetes/src/v2/kubernetes.v2.module.ts
+++ b/app/scripts/modules/kubernetes/src/v2/kubernetes.v2.module.ts
@@ -1,6 +1,6 @@
import { module } from 'angular';
-import { CloudProviderRegistry } from '@spinnaker/core';
+import { CloudProviderRegistry, STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT } from '@spinnaker/core';
import '../logo/kubernetes.logo.less';
import { KUBERNETES_MANIFEST_BASIC_SETTINGS } from './manifest/wizard/basicSettings.component';
@@ -95,6 +95,7 @@ module(KUBERNETES_V2_MODULE, [
JSON_EDITOR_COMPONENT,
KUBERNETES_ENABLE_MANIFEST_STAGE,
KUBERNETES_DISABLE_MANIFEST_STAGE,
+ STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT,
]).config(() => {
CloudProviderRegistry.registerProvider('kubernetes', {
name: 'Kubernetes',
diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestBindArtifactsSelector.tsx b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestBindArtifactsSelector.tsx
new file mode 100644
index 00000000000..c04a4fdca00
--- /dev/null
+++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestBindArtifactsSelector.tsx
@@ -0,0 +1,84 @@
+import { module } from 'angular';
+import * as React from 'react';
+import { react2angular } from 'react2angular';
+
+import {
+ IArtifact,
+ IExpectedArtifact,
+ IPipeline,
+ IStage,
+ ArtifactTypePatterns,
+ StageArtifactSelector,
+} from '@spinnaker/core';
+
+export interface IManifestBindArtifact {
+ expectedArtifactId?: string;
+ artifact?: IArtifact;
+}
+
+export interface IManifestBindArtifactsSelector {
+ pipeline: IPipeline;
+ stage: IStage;
+ bindings?: IManifestBindArtifact[];
+ onChangeBindings: (_: IManifestBindArtifact[]) => void;
+}
+
+export class ManifestBindArtifactsSelector extends React.Component {
+ private onChangeBinding = (index: number, binding: IManifestBindArtifact) => {
+ const bindings = (this.props.bindings || []).slice(0);
+ bindings[index] = binding;
+ this.props.onChangeBindings(bindings);
+ };
+
+ private onRemoveBinding = (index: number) => {
+ const bindings = (this.props.bindings || []).slice(0);
+ bindings.splice(index, 1);
+ this.props.onChangeBindings(bindings);
+ };
+
+ public render() {
+ const { stage, pipeline, bindings } = this.props;
+
+ const renderSelect = (i: number, binding?: IManifestBindArtifact) => {
+ const key = (!binding && 'new') || binding.expectedArtifactId || (binding.artifact && binding.artifact.id);
+ return (
+
+
+ this.onChangeBinding(i, { artifact: artifact })}
+ onExpectedArtifactSelected={(expectedArtifact: IExpectedArtifact) =>
+ this.onChangeBinding(i, { expectedArtifactId: expectedArtifact.id })
+ }
+ excludedArtifactIds={bindings.map(b => b.expectedArtifactId)}
+ excludedArtifactTypePatterns={[ArtifactTypePatterns.FRONT50_PIPELINE_TEMPLATE]}
+ />
+
+ {binding && (
+
+ )}
+
+ );
+ };
+ const renderSelectEditable = (binding: IManifestBindArtifact, i: number) => renderSelect(i, binding);
+
+ return (
+ <>
+ {bindings.map(renderSelectEditable)}
+ {renderSelect(bindings.length)}
+ >
+ );
+ }
+}
+
+export const MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT =
+ 'spinnaker.kubernetes.v2.pipelines.deployManifest.bindArtifacts.selector.react';
+module(MANIFEST_BIND_ARTIFACTS_SELECTOR_REACT, []).component(
+ 'manifestBindArtifactsSelectorReact',
+ react2angular(ManifestBindArtifactsSelector, ['pipeline', 'stage', 'bindings', 'onChangeBindings']),
+);
diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts
index c36b7da4766..8b7c4fefbc4 100644
--- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts
+++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.controller.ts
@@ -1,6 +1,14 @@
import { IController, IScope } from 'angular';
import { get, defaults } from 'lodash';
-import { ExpectedArtifactSelectorViewController, NgGenericArtifactDelegate, IManifest } from '@spinnaker/core';
+import {
+ ExpectedArtifactSelectorViewController,
+ NgGenericArtifactDelegate,
+ IManifest,
+ IArtifact,
+ IExpectedArtifact,
+ ArtifactTypePatterns,
+ SETTINGS,
+} from '@spinnaker/core';
import {
IKubernetesManifestCommandMetadata,
@@ -8,6 +16,8 @@ import {
KubernetesManifestCommandBuilder,
} from 'kubernetes/v2/manifest/manifestCommandBuilder.service';
+import { IManifestBindArtifact } from './ManifestBindArtifactsSelector';
+
export class KubernetesV2DeployManifestConfigCtrl implements IController {
public state = {
loaded: false,
@@ -16,20 +26,34 @@ export class KubernetesV2DeployManifestConfigCtrl implements IController {
public metadata: IKubernetesManifestCommandMetadata;
public textSource = 'text';
public artifactSource = 'artifact';
- public sources = [this.textSource, this.artifactSource];
-
public manifestArtifactDelegate: NgGenericArtifactDelegate;
public manifestArtifactController: ExpectedArtifactSelectorViewController;
+ public sources = [this.textSource, this.artifactSource];
public static $inject = ['$scope'];
+
constructor(private $scope: IScope) {
+ this.manifestArtifactDelegate = new NgGenericArtifactDelegate($scope, 'manifest');
+ this.manifestArtifactController = new ExpectedArtifactSelectorViewController(this.manifestArtifactDelegate);
+
+ const stage = this.$scope.stage;
+ this.$scope.bindings = (stage.requiredArtifactIds || [])
+ .map((id: string) => ({ expectedArtifactId: id }))
+ .concat((stage.requiredArtifacts || []).map((artifact: IArtifact) => ({ artifact: artifact })));
+
+ this.$scope.excludedManifestArtifactTypes = [
+ ArtifactTypePatterns.DOCKER_IMAGE,
+ ArtifactTypePatterns.KUBERNETES,
+ ArtifactTypePatterns.FRONT50_PIPELINE_TEMPLATE,
+ ];
+
KubernetesManifestCommandBuilder.buildNewManifestCommand(
this.$scope.application,
- this.$scope.stage.manifests || this.$scope.stage.manifest,
- this.$scope.stage.moniker,
+ stage.manifests || stage.manifest,
+ stage.moniker,
).then((builtCommand: IKubernetesManifestCommandData) => {
- if (this.$scope.stage.isNew) {
- defaults(this.$scope.stage, builtCommand.command, {
+ if (stage.isNew) {
+ defaults(stage, builtCommand.command, {
manifestArtifactAccount: '',
source: this.textSource,
});
@@ -39,11 +63,28 @@ export class KubernetesV2DeployManifestConfigCtrl implements IController {
this.manifestArtifactDelegate.setAccounts(get(this, ['metadata', 'backingData', 'artifactAccounts'], []));
this.manifestArtifactController.updateAccounts(this.manifestArtifactDelegate.getSelectedExpectedArtifact());
});
-
- this.manifestArtifactDelegate = new NgGenericArtifactDelegate($scope, 'manifest');
- this.manifestArtifactController = new ExpectedArtifactSelectorViewController(this.manifestArtifactDelegate);
}
+ public onManifestExpectedArtifactSelected = (expectedArtifact: IExpectedArtifact) => {
+ this.$scope.$applyAsync(() => {
+ this.$scope.stage.manifestArtifactId = expectedArtifact.id;
+ });
+ };
+
+ public onManifestArtifactEdited = (artifact: IArtifact) => {
+ this.$scope.$applyAsync(() => {
+ this.$scope.stage.manifestArtifact = artifact;
+ });
+ };
+
+ public onRequiredArtifactsChanged = (bindings: IManifestBindArtifact[]) => {
+ this.$scope.$applyAsync(() => {
+ this.$scope.bindings = bindings;
+ this.$scope.stage.requiredArtifactIds = bindings.filter((b: IManifestBindArtifact) => b.expectedArtifactId);
+ this.$scope.stage.requiredArtifacts = bindings.filter((b: IManifestBindArtifact) => b.artifact);
+ });
+ };
+
public canShowAccountSelect() {
return (
this.$scope.stage.source === this.artifactSource &&
@@ -58,4 +99,8 @@ export class KubernetesV2DeployManifestConfigCtrl implements IController {
// This method is called from a React component.
this.$scope.$applyAsync();
};
+
+ public checkFeatureFlag(flag: string): boolean {
+ return !!SETTINGS.feature[flag];
+ }
}
diff --git a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html
index 6f039bd3371..ce84ab64dc3 100644
--- a/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html
+++ b/app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/deployManifestConfig.html
@@ -24,47 +24,81 @@ Manifest Configuration
-
-
+
+
+
+
+
+
+
+
+
+