import { UISref } from '@uirouter/react'; import type { IHttpPromiseCallbackArg } from 'angular'; import { cloneDeep, get, uniqBy } from 'lodash'; import { Debounce } from 'lodash-decorators'; import { $log } from 'ngimport'; import React from 'react'; import { Button, Modal } from 'react-bootstrap'; import type { Option } from 'react-select'; import Select from 'react-select'; import { ManagedTemplateSelector } from './ManagedTemplateSelector'; import { TemplateDescription } from './TemplateDescription'; import type { Application } from '../../application/application.model'; import { PipelineConfigService } from '../config/services/PipelineConfigService'; import { SETTINGS } from '../../config/settings'; import type { IPipelineTemplate, IPipelineTemplateConfig } from '../config/templates/PipelineTemplateReader'; import { PipelineTemplateReader } from '../config/templates/PipelineTemplateReader'; import { PipelineTemplateV2Service } from '../config/templates/v2/pipelineTemplateV2.service'; import type { IPipeline } from '../../domain/IPipeline'; import type { IPipelineTemplateV2 } from '../../domain/IPipelineTemplateV2'; import { SubmitButton } from '../../modal/buttons/SubmitButton'; import { Overridable } from '../../overrideRegistry'; import { Spinner } from '../../widgets/spinners/Spinner'; import './createPipelineModal.less'; export interface ICreatePipelineModalState { submitting: boolean; saveError: boolean; saveErrorMessage: string; loading: boolean; loadError: boolean; loadErrorMessage: string; command: ICreatePipelineCommand; existingNames: string[]; configs: Array>; configOptions: Option[]; templates: IPipelineTemplate[]; useTemplate: boolean; useManagedTemplate: boolean; loadingTemplateFromSource: boolean; loadingTemplateFromSourceError: boolean; templateSourceUrl: string; inheritTemplateParameters: boolean; inheritTemplateExpectedArtifacts: boolean; inheritTemplateTriggers: boolean; } export interface ICreatePipelineCommand { strategy: boolean; name: string; config: Partial; template: IPipelineTemplate; } export interface ICreatePipelineModalProps { application: Application; pipelineSavedCallback: (pipelineId: string) => void; show: boolean; showCallback: (show: boolean) => void; preselectedTemplate?: IPipelineTemplateV2; } @Overridable('core.pipeline.CreatePipelineModal') export class CreatePipelineModal extends React.Component { constructor(props: ICreatePipelineModalProps) { super(props); this.state = this.getDefaultState(); } public static defaultProps: Partial = { preselectedTemplate: null, }; public componentDidUpdate(prevProps: ICreatePipelineModalProps): void { if (!prevProps.show && this.props.show && !this.state.loading) { this.loadPipelineTemplates(); } } private getDefaultConfig(): Partial { return { name: 'None', stages: [], triggers: [], application: this.props.application.name, limitConcurrent: true, keepWaitingPipelines: false, spelEvaluator: 'v4', }; } private getDefaultState(): ICreatePipelineModalState { const defaultConfig = this.getDefaultConfig(); const { application } = this.props; const configs: Array> = [defaultConfig].concat(get(application, 'pipelineConfigs.data', [])); const configOptions: Option[] = configs.map((config) => ({ value: config.name, label: config.name })); const existingNames: string[] = [defaultConfig] .concat(get(application, 'pipelineConfigs.data', [])) .concat(get(application, 'strategyConfigs.data', [])) .map((config) => config.name); return { submitting: false, saveError: false, saveErrorMessage: null, loading: false, loadError: false, loadErrorMessage: null, configs, configOptions, templates: [], existingNames, command: { strategy: false, name: '', config: defaultConfig, template: null }, useTemplate: false, useManagedTemplate: true, loadingTemplateFromSource: false, loadingTemplateFromSourceError: false, templateSourceUrl: '', inheritTemplateParameters: true, inheritTemplateExpectedArtifacts: true, inheritTemplateTriggers: true, }; } public submit = (): void => { const command = cloneDeep(this.state.command); const pipelineConfig: Partial = command.strategy ? this.getDefaultConfig() : command.config; pipelineConfig.name = command.name.trim(); pipelineConfig.index = this.props.application.getDataSource('pipelineConfigs').data.length; delete pipelineConfig.id; if (command.strategy) { pipelineConfig.strategy = true; pipelineConfig.limitConcurrent = false; } if (pipelineConfig.type === 'templatedPipeline') { delete pipelineConfig.config.pipeline.pipelineConfigId; pipelineConfig.config.pipeline.name = command.name; } this.setState({ submitting: true }); PipelineConfigService.savePipeline(pipelineConfig as IPipeline).then( () => this.onSaveSuccess(pipelineConfig), this.onSaveFailure, ); }; private submitPipelineTemplateConfig = (): void => { const { application, preselectedTemplate } = this.props; const { command } = this.state; const pipelineConfig: Partial = { name: command.name, application: application.name, type: 'templatedPipeline', limitConcurrent: true, keepWaitingPipelines: false, triggers: [], }; const config = { ...pipelineConfig, ...(preselectedTemplate ? PipelineTemplateV2Service.getPipelineTemplateConfigV2( PipelineTemplateV2Service.getTemplateVersion(preselectedTemplate), ) : PipelineTemplateReader.getPipelineTemplateConfig({ name: command.name, application: application.name, source: command.template.selfLink, })), }; this.setState({ submitting: true }); PipelineConfigService.savePipeline(config as IPipeline).then(() => this.onSaveSuccess(config), this.onSaveFailure); }; private onSaveSuccess(config: Partial): void { const application = this.props.application; application.pipelineConfigs.refresh(true).then(() => { const configs: IPipeline[] = config.strategy ? application.strategyConfigs.data : application.pipelineConfigs.data; const newPipeline = configs.find((_config) => _config.name === config.name); if (!newPipeline) { $log.warn('Could not find new pipeline after save succeeded.'); this.setState({ saveError: true, saveErrorMessage: 'Sorry, there was an error retrieving your new pipeline. Please refresh the browser.', submitting: false, }); } else { newPipeline.isNew = true; this.setState(this.getDefaultState()); this.props.pipelineSavedCallback(newPipeline.id); } }); } private onSaveFailure = (response: IHttpPromiseCallbackArg<{ message: string }>): void => { $log.warn(response); this.setState({ submitting: false, saveError: true, saveErrorMessage: (response && response.data && response.data.message) || 'No message provided', }); }; public close = (evt?: React.MouseEvent): void => { evt && evt.stopPropagation(); this.setState(this.getDefaultState()); this.props.showCallback(false); }; private handleTypeChange = (option: Option): void => { const strategy = option.value; this.setState({ command: { ...this.state.command, strategy } }); }; private handleNameChange = (e: React.ChangeEvent): void => { this.setState({ command: { ...this.state.command, name: e.target.value } }); }; private handleConfigChange = (option: Option): void => { const config = this.state.configs.find((t) => t.name === option.value); this.setState({ command: { ...this.state.command, config } }); }; private handleSaveErrorDismiss = (): void => { this.setState({ saveError: false }); }; private handleLoadErrorDismiss = (): void => { this.setState({ loadError: false }); }; private handleTemplateSelection = (template: IPipelineTemplate): void => { this.setState({ command: { ...this.state.command, template } }); }; private handleUseTemplateSelection(useTemplate: boolean): () => void { return () => this.setState({ useTemplate }); } private handleUseManagedTemplateSelection(useManagedTemplate: boolean): () => void { return () => { this.setState({ useManagedTemplate, templateSourceUrl: '', loadingTemplateFromSourceError: false, command: { ...this.state.command, template: null }, }); }; } public handleSourceUrlChange = (e: React.ChangeEvent): void => { const templateSourceUrl = e.target.value; this.setState({ templateSourceUrl }); this.loadPipelineTemplateFromSource(templateSourceUrl); }; private configOptionRenderer = (option: Option) => { const config = this.state.configs.find((t) => t.name === option.value); return (
{config.name}
{config.stages.length > 0 && (
Stages:
    {config.stages.map((stage) => (
  • {stage.name || stage.type}
  • ))}
)}
); }; public validateNameCharacters(): boolean { return /^[^\\^/?%#]*$/.test(this.state.command.name); // Verify name does not include: \, ^, ?, %, # } public validateNameIsUnique(): boolean { return this.state.existingNames.every((name) => name !== this.state.command.name.trim()); } public loadPipelineTemplates(): void { if (SETTINGS.feature.pipelineTemplates) { this.setState({ loading: true }); PipelineTemplateReader.getPipelineTemplatesByScopes([this.props.application.name, 'global']) .then((templates) => { templates = uniqBy(templates, 'id').filter(({ schema }) => schema !== 'v2'); this.setState({ templates, loading: false }); }) .catch((response: IHttpPromiseCallbackArg<{ message: string }>) => { this.setState({ loadError: true, loadErrorMessage: (response && response.data && response.data.message) || 'No message provided', loading: false, }); }) .finally(() => { if (!this.state.templates.length) { this.setState({ useManagedTemplate: false }); } }); } } @Debounce(200) private loadPipelineTemplateFromSource(sourceUrl: string): void { if (sourceUrl) { this.setState({ loadingTemplateFromSource: true, loadingTemplateFromSourceError: false }); PipelineTemplateReader.getPipelineTemplateFromSourceUrl(sourceUrl) .then((template) => (this.state.command.template = template)) .catch(() => this.setState({ loadingTemplateFromSourceError: true })) .finally(() => this.setState({ loadingTemplateFromSource: false })); } } // Prevents the form from reloading the page if the user hits enter on an input. private handleFormSubmit = (e: React.FormEvent) => e.preventDefault(); public render() { const { preselectedTemplate } = this.props; const hasSelectedATemplate = this.state.useTemplate || preselectedTemplate; const nameHasError = !this.validateNameCharacters(); const nameIsNotUnique = !this.validateNameIsUnique(); const formValid = !nameHasError && !nameIsNotUnique && this.state.command.name.length > 0 && (!this.state.useTemplate || !!this.state.command.template); return ( Create New {this.state.command.strategy ? 'Strategy' : 'Pipeline'} {this.state.loading && ( )} {!this.state.loading && ( {this.state.loadError && (

Could not load pipeline templates.

Reason: {this.state.loadErrorMessage}

[dismiss]

)} {this.state.saveError && (

Could not save pipeline.

Reason: {this.state.saveErrorMessage}

[dismiss]

)} {!(this.state.saveError || this.state.loadError) && (
{!preselectedTemplate && (
Type
{nameHasError && (
{this.state.command.strategy ? 'Strategy' : 'Pipeline'} name cannot contain any of the following characters:
/ \ ? % #
)} {nameIsNotUnique && (
There is already a {this.state.command.strategy ? 'strategy' : 'pipeline'} with that name.
)} {SETTINGS.feature.pipelineTemplates && !preselectedTemplate && !this.state.command.strategy && (
Create From
)} {this.state.configs.length > 1 && !this.state.command.strategy && !this.state.useTemplate && (
{SETTINGS.feature.pipelineTemplates &&
}
Copy From
Managed Templates
)} {this.state.useManagedTemplate && !preselectedTemplate && ( )} {!this.state.useManagedTemplate && (
{this.state.templates.length === 0 &&
Source URL
}
)} {!preselectedTemplate && (
* v1 templates only. For creating pipelines from v2 templates, use the{' '} Pipeline Templates view.
)}
)}
)}
)} {SETTINGS.feature.pipelineTemplates && hasSelectedATemplate && ( )} {!hasSelectedATemplate && ( )}
); } }