diff --git a/app/scripts/modules/core/src/domain/ITrigger.ts b/app/scripts/modules/core/src/domain/ITrigger.ts index 8a08bfe86f5..de73b9c93f4 100644 --- a/app/scripts/modules/core/src/domain/ITrigger.ts +++ b/app/scripts/modules/core/src/domain/ITrigger.ts @@ -18,6 +18,7 @@ export interface IArtifactoryTrigger extends ITrigger { export interface IGitTrigger extends ITrigger { source: string; + secret?: string; project: string; slug: string; branch: string; @@ -30,14 +31,22 @@ export interface IBuildTrigger extends ITrigger { buildNumber?: number; job: string; project: string; + propertyFile?: string; master: string; type: 'jenkins' | 'travis' | 'wercker' | 'concourse'; } +export interface IWerckerTrigger extends IBuildTrigger { + app: string; + pipeline: string; + type: 'wercker'; +} + export interface IConcourseTrigger extends IBuildTrigger { // Concourse pipeline is represented by project team: string; jobName: string; // job will be the concatenation of team/pipeline/jobName + type: 'concourse'; } export interface IPipelineTrigger extends ITrigger { @@ -45,6 +54,7 @@ export interface IPipelineTrigger extends ITrigger { parentExecution?: IExecution; parentPipelineId?: string; pipeline: string; + status: string[]; } export interface ICronTrigger extends ITrigger { diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/BaseTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/BaseTrigger.tsx new file mode 100644 index 00000000000..51b9c6c2626 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/BaseTrigger.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; + +import { Observable, Subject } from 'rxjs'; + +import { ITrigger } from 'core/domain'; +import { SETTINGS } from 'core/config/settings'; +import { ServiceAccountReader } from 'core/serviceAccount/ServiceAccountReader'; +import { RunAsUser } from 'core/pipeline'; + +export interface IBaseTriggerConfigProps { + triggerContents: React.ReactNode; + trigger: ITrigger; + triggerUpdated?: (trigger: ITrigger) => void; +} + +export interface IBaseTriggerState { + serviceAccounts?: string[]; +} + +export class BaseTrigger extends React.Component { + private destroy$ = new Subject(); + + constructor(props: IBaseTriggerConfigProps) { + super(props); + this.state = { + serviceAccounts: [], + }; + } + + public componentDidMount(): void { + if (SETTINGS.feature.fiatEnabled) { + Observable.fromPromise(ServiceAccountReader.getServiceAccounts()) + .takeUntil(this.destroy$) + .subscribe(serviceAccounts => this.setState({ serviceAccounts })); + } + } + + public componentWillUnmount(): void { + this.destroy$.next(); + } + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + private renderRunAsUser = (): React.ReactNode => { + const { trigger } = this.props; + const { serviceAccounts } = this.state; + return ( + <> + {SETTINGS.feature.fiatEnabled && serviceAccounts && ( +
+ this.onUpdateTrigger({ user })} + value={trigger.user} + selectColumns={6} + /> +
+ )} + + ); + }; + + public render() { + const { triggerContents } = this.props; + const { renderRunAsUser } = this; + return ( + <> + {triggerContents} + {renderRunAsUser()} + + ); + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/ITriggerConfigProps.ts b/app/scripts/modules/core/src/pipeline/config/triggers/ITriggerConfigProps.ts deleted file mode 100644 index 7d705396dbd..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/ITriggerConfigProps.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ITrigger } from 'core/domain'; - -export interface ITriggerConfigProps { - fieldUpdated: () => void; - trigger: ITrigger; -} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/RunAsUser.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/RunAsUser.tsx index 752d747033b..c6b36781d15 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/RunAsUser.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/RunAsUser.tsx @@ -24,7 +24,7 @@ export class RunAsUser extends React.Component { return (
- Run As User + Run As User
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/ArtifactoryTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/ArtifactoryTrigger.tsx new file mode 100644 index 00000000000..7907804d02c --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/ArtifactoryTrigger.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; + +import { Observable, Subject } from 'rxjs'; + +import { IArtifactoryTrigger } from 'core/domain/ITrigger'; +import { BaseTrigger } from 'core/pipeline'; +import { ArtifactoryReaderService } from './artifactoryReader.service'; +import { Application } from 'core'; + +export interface IArtifactoryTriggerConfigProps { + trigger: IArtifactoryTrigger; + pipelineId: string; + application: Application; + triggerUpdated: (trigger: IArtifactoryTrigger) => void; +} + +export interface IArtifactoryTriggerConfigState { + artifactorySearchNames: string[]; +} + +export class ArtifactoryTrigger extends React.Component< + IArtifactoryTriggerConfigProps, + IArtifactoryTriggerConfigState +> { + private destroy$ = new Subject(); + + constructor(props: IArtifactoryTriggerConfigProps) { + super(props); + this.state = { + artifactorySearchNames: [], + }; + } + + public componentDidMount() { + Observable.fromPromise(ArtifactoryReaderService.getArtifactoryNames()) + .takeUntil(this.destroy$) + .subscribe((artifactorySearchNames: string[]) => { + this.setState({ artifactorySearchNames }); + }); + } + + public componentWillUnmount(): void { + this.destroy$.next(); + } + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + private ArtifactoryTriggerContents = () => { + const { artifactorySearchNames } = this.state; + const { artifactorySearchName } = this.props.trigger; + return ( + <> +
+
+ Artifactory Name +
+
+ ({ label: name, value: name }))} - clearable={false} - /> - -
-
-
- ); - } -} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactoryTrigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactory.trigger.ts similarity index 77% rename from app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactoryTrigger.ts rename to app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactory.trigger.ts index 359f1ef3fa5..25826212723 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactoryTrigger.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifactory/artifactory.trigger.ts @@ -1,12 +1,12 @@ import { Registry } from 'core/registry'; -import { ArtifactoryTriggerConfig } from './ArtifactoryTriggerConfig'; +import { ArtifactoryTrigger } from './ArtifactoryTrigger'; import { ArtifactTypePatterns, excludeAllTypesExcept } from 'core/artifact'; Registry.pipeline.registerTrigger({ label: 'Artifactory', description: 'Executes the pipeline on an Artifactory repo update', key: 'artifactory', - component: ArtifactoryTriggerConfig, + component: ArtifactoryTrigger, validators: [], excludedArtifactTypePatterns: excludeAllTypesExcept(ArtifactTypePatterns.MAVEN_FILE), }); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTrigger.tsx new file mode 100644 index 00000000000..d6fad92b068 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTrigger.tsx @@ -0,0 +1,209 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; +import { Observable, Subject } from 'rxjs'; + +import { BaseTrigger } from 'core/pipeline'; +import { BuildServiceType, IgorService } from 'core/ci/igor.service'; +import { IBuildTrigger } from 'core/domain'; +import { HelpField } from 'core/help'; +import { Spinner } from 'core/widgets'; +import { TextInput, Tooltip } from 'core/presentation'; + +export interface IBaseBuildTriggerConfigProps { + buildTriggerType: BuildServiceType; + trigger: IBuildTrigger; + triggerUpdated: (trigger: IBuildTrigger) => void; +} + +export interface IBaseBuildTriggerState { + jobs: string[]; + jobsLoaded: boolean; + jobsRefreshing: boolean; + masters: string[]; + mastersLoaded: boolean; + mastersRefreshing: boolean; +} + +export class BaseBuildTrigger extends React.Component { + private destroy$ = new Subject(); + + constructor(props: IBaseBuildTriggerConfigProps) { + super(props); + this.state = { + jobs: [], + jobsLoaded: false, + jobsRefreshing: false, + masters: [], + mastersLoaded: false, + mastersRefreshing: false, + }; + } + + public componentDidMount = () => { + this.initializeMasters(); + }; + + private refreshMasters = () => { + this.setState({ + mastersRefreshing: true, + }); + this.initializeMasters(); + }; + + private refreshJobs = () => { + this.setState({ + jobsRefreshing: true, + }); + this.updateJobsList(this.props.trigger.master); + }; + + private jobsUpdated = (jobs: string[]) => { + this.setState({ + jobs, + jobsLoaded: true, + jobsRefreshing: false, + }); + if (jobs.length && !jobs.includes(this.props.trigger.job)) { + this.onUpdateTrigger({ job: '' }); + } + }; + + private updateJobsList = (master: string) => { + if (master) { + this.setState({ + jobsLoaded: false, + jobs: [], + }); + Observable.fromPromise(IgorService.listJobsForMaster(master)) + .takeUntil(this.destroy$) + .subscribe(this.jobsUpdated, () => this.jobsUpdated([])); + } + }; + + private onMasterUpdated = (option: Option) => { + const master = option.value; + if (this.props.trigger.master !== master) { + this.onUpdateTrigger({ master }); + this.updateJobsList(master); + } + }; + + private initializeMasters = () => { + Observable.fromPromise(IgorService.listMasters(this.props.buildTriggerType)) + .takeUntil(this.destroy$) + .subscribe(this.mastersUpdated, () => this.mastersUpdated([])); + }; + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + private mastersUpdated = (masters: string[]) => { + this.setState({ + masters, + mastersLoaded: true, + mastersRefreshing: false, + }); + if (this.props.trigger.master) { + this.refreshJobs(); + } + }; + + private Jobs = () => { + const { job } = this.props.trigger; + const { jobs, jobsLoaded } = this.state; + return ( + <> + {jobsLoaded && ( + ({ label: m, value: m }))} + placeholder={'Select a master...'} + value={master} + /> +
+
+ + + +
+ +
+ +
+ {!master &&

(Select a master)

} + {master && } +
+
+ {master && ( + + + + )} +
+
+
+
+ Property File + +
+
+ ) => + this.onUpdateTrigger({ propertyFile: event.target.value }) + } + value={propertyFile} + /> +
+
+ + ); + }; + + public render() { + const { BaseBuildTriggerContents } = this; + return } />; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTriggerTemplate.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTriggerTemplate.tsx new file mode 100644 index 00000000000..73f7f60c6c9 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/baseBuild/BaseBuildTriggerTemplate.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; + +import { capitalize } from 'lodash'; +import { Option } from 'react-select'; +import { $q } from 'ngimport'; +import { IPromise } from 'angular'; + +import { Observable, Subject } from 'rxjs'; + +import { IBuild, IBuildInfo, IBuildTrigger } from 'core/domain'; +import { ITriggerTemplateComponentProps } from 'core/pipeline/manualExecution/TriggerTemplate'; +import { IgorService, BuildServiceType } from 'core/ci'; +import { Spinner } from 'core/widgets/spinners/Spinner'; +import { buildDisplayName } from 'core/pipeline/executionBuild/buildDisplayName.filter'; +import { timestamp } from 'core/utils/timeFormatters'; +import { TetheredSelect } from 'core/presentation/TetheredSelect'; + +export interface IBaseBuildTriggerTemplateProps extends ITriggerTemplateComponentProps { + buildTriggerType: BuildServiceType; + optionRenderer?: (build: Option) => JSX.Element; +} + +export interface IBaseBuildTriggerTemplateState { + builds?: IBuild[]; + buildsLoading?: boolean; + loadError?: boolean; + selectedBuild?: number; +} + +export class BaseBuildTriggerTemplate extends React.Component< + IBaseBuildTriggerTemplateProps, + IBaseBuildTriggerTemplateState +> { + private destroy$ = new Subject(); + + public static formatLabel(trigger: IBuildTrigger): IPromise { + return $q.when(`(${capitalize(trigger.type)}) ${trigger.master}: ${trigger.job}`); + } + + public constructor(props: IBaseBuildTriggerTemplateProps) { + super(props); + this.state = { + builds: [], + buildsLoading: false, + loadError: false, + selectedBuild: 0, + }; + } + + private buildLoadSuccess = (allBuilds: IBuild[]) => { + const newState: Partial = { + buildsLoading: false, + }; + + const trigger = this.props.command.trigger as IBuildTrigger; + newState.builds = allBuilds + .filter(build => !build.building && build.result === 'SUCCESS') + .sort((a, b) => b.number - a.number); + if (newState.builds.length) { + // default to what is supplied by the trigger if possible; otherwise, use the latest + const defaultSelection = newState.builds.find(b => b.number === trigger.buildNumber) || newState.builds[0]; + newState.selectedBuild = defaultSelection.number; + this.updateSelectedBuild(defaultSelection); + } + + this.setState(newState); + }; + + private buildLoadFailure = () => { + this.setState({ + buildsLoading: false, + loadError: true, + }); + }; + + private updateSelectedBuild = (item: any) => { + this.props.command.extraFields.buildNumber = item.number; + this.props.command.triggerInvalid = false; + this.setState({ selectedBuild: item.number }); + }; + + private initialize = () => { + const { command } = this.props; + command.triggerInvalid = true; + const trigger = command.trigger as IBuildTrigger; + + // These fields will be added to the trigger when the form is submitted + command.extraFields = {}; + + this.setState({ + buildsLoading: true, + loadError: false, + }); + + if (trigger.buildNumber) { + this.updateSelectedBuild(trigger.buildInfo); + } + + // do not re-initialize if the trigger has changed to some other type + if (trigger.type !== this.props.buildTriggerType) { + return; + } + + Observable.fromPromise(IgorService.listBuildsForJob(trigger.master, trigger.job)) + .takeUntil(this.destroy$) + .subscribe(this.buildLoadSuccess, this.buildLoadFailure); + }; + + public componentDidMount() { + this.initialize(); + } + + public componentWillUnmount(): void { + this.destroy$.next(); + } + + public componentWillReceiveProps(nextProps: ITriggerTemplateComponentProps) { + if (nextProps.command !== this.props.command) { + this.initialize(); + } + } + + private handleBuildChanged = (option: Option): void => { + this.updateSelectedBuild({ number: option.number }); + }; + + private optionRenderer = (build: Option) => { + return ( + + Build {build.number} + {buildDisplayName(build as IBuildInfo)}({timestamp(build.timestamp)}) + + ); + }; + + public render() { + const { builds, buildsLoading, loadError, selectedBuild } = this.state; + + return ( +
+ + {buildsLoading && ( +
+
+ +
+
+ )} + {loadError &&
Error loading builds!
} + {!buildsLoading && ( +
+ {builds.length === 0 && ( +
+

No builds found

+
+ )} + {builds.length > 0 && ( + + )} +
+ )} +
+ ); + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerConfig.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTrigger.tsx similarity index 53% rename from app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerConfig.tsx rename to app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTrigger.tsx index 353d6aa9d2b..c9ffaad0961 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerConfig.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTrigger.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; import Select, { Option } from 'react-select'; +import { BaseTrigger } from 'core/pipeline'; import { IConcourseTrigger } from 'core/domain'; -import { ServiceAccountReader } from 'core/serviceAccount'; -import { SETTINGS } from 'core/config/settings'; +import { Observable } from 'rxjs'; import { BuildServiceType, IgorService } from 'core/ci'; -import { ITriggerConfigProps } from '../ITriggerConfigProps'; -import { RunAsUser } from '../RunAsUser'; import { ConcourseService } from './concourse.service'; +import { Subject } from 'rxjs/Subject'; -export interface IConcourseTriggerConfigProps extends ITriggerConfigProps { +export interface IConcourseTriggerConfigProps { trigger: IConcourseTrigger; + triggerUpdated: (trigger: IConcourseTrigger) => void; } export interface IConcourseTriggerConfigState { @@ -18,13 +18,11 @@ export interface IConcourseTriggerConfigState { teams: string[]; pipelines: string[]; jobs: string[]; - serviceAccounts: string[]; } -export class ConcourseTriggerConfig extends React.Component< - IConcourseTriggerConfigProps, - IConcourseTriggerConfigState -> { +export class ConcourseTrigger extends React.Component { + private destroy$ = new Subject(); + constructor(props: IConcourseTriggerConfigProps) { super(props); const { master, team, project, job } = this.props.trigger; @@ -33,29 +31,120 @@ export class ConcourseTriggerConfig extends React.Component< teams: team ? [team] : [], pipelines: project ? [project.split('/').pop()] : [], jobs: job ? [job] : [], - serviceAccounts: [], }; } public componentDidMount(): void { - if (SETTINGS.feature.fiat) { - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.setState({ serviceAccounts: accounts || [] }); + Observable.fromPromise(IgorService.listMasters(BuildServiceType.Concourse)) + .takeUntil(this.destroy$) + .subscribe((masters: string[]) => { + this.setState({ masters }); + }); + + const { trigger } = this.props; + this.fetchAvailableTeams(trigger); + this.fetchAvailablePipelines(trigger); + this.fetchAvailableJobs(trigger); + } + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, }); + }; + + private onTeamChanged = (option: Option) => { + const team = option.value; + if (this.props.trigger.team !== team) { + const trigger = { + ...this.props.trigger, + job: '', + jobName: '', + project: '', + team, + }; + this.fetchAvailablePipelines(trigger); + this.onUpdateTrigger(trigger); } + }; - IgorService.listMasters(BuildServiceType.Concourse).then((masters: string[]) => { - this.setState({ masters }); - }); + private onPipelineChanged = (option: Option) => { + const p = option.value; + const { project, team } = this.props.trigger; + + if (!project || project.split('/').pop() !== p) { + const trigger = { + ...this.props.trigger, + job: '', + jobName: '', + project: `${team}/${p}`, + }; + this.fetchAvailableJobs(trigger); + this.onUpdateTrigger(trigger); + } + }; - this.fetchAvailableTeams(); - this.fetchAvailablePipelines(); - this.fetchAvailableJobs(); - } + private onJobChanged = (option: Option) => { + const jobName = option.value; + + if (this.props.trigger.jobName !== jobName) { + const { project } = this.props.trigger; + const trigger = { + ...this.props.trigger, + jobName, + job: `${project}/${jobName}`, + }; + this.fetchAvailableJobs(trigger); + this.onUpdateTrigger(trigger); + } + }; - public render() { - const { jobName, team, project, master, runAsUser } = this.props.trigger; - const { jobs, pipelines, teams, masters, serviceAccounts } = this.state; + private onMasterChanged = (option: Option) => { + const master = option.value; + + if (this.props.trigger.master !== master) { + const trigger = { + ...this.props.trigger, + master, + }; + this.fetchAvailableTeams(trigger); + this.onUpdateTrigger(trigger); + } + }; + + private fetchAvailableTeams = (trigger: IConcourseTrigger) => { + const { master } = trigger; + if (master) { + Observable.fromPromise(ConcourseService.listTeamsForMaster(master)) + .takeUntil(this.destroy$) + .subscribe(teams => this.setState({ teams })); + } + }; + + private fetchAvailablePipelines = (trigger: IConcourseTrigger) => { + const { master, team } = trigger; + if (master && team) { + Observable.fromPromise(ConcourseService.listPipelinesForTeam(master, team)) + .takeUntil(this.destroy$) + .subscribe(pipelines => this.setState({ pipelines })); + } + }; + + private fetchAvailableJobs = (trigger: IConcourseTrigger) => { + const { master, project, team } = trigger; + if (master && team && project) { + const pipeline = project.split('/').pop(); + Observable.fromPromise(ConcourseService.listJobsForPipeline(master, team, pipeline)) + .takeUntil(this.destroy$) + .subscribe(jobs => this.setState({ jobs })); + } + }; + + private ConcourseTriggerContents = () => { + const { jobName, team, project, master } = this.props.trigger; + const { jobs, pipelines, teams, masters } = this.state; const pipeline = project && project.split('/').pop(); return ( @@ -120,86 +209,12 @@ export class ConcourseTriggerConfig extends React.Component< )} - {SETTINGS.feature.fiatEnabled && !SETTINGS.feature.managedServiceAccounts && ( -
- - /> -
- )} ); - } - - private onRunAsUserChanged = (runAsUser: string) => { - this.props.trigger.runAsUser = runAsUser; - this.props.fieldUpdated(); - }; - - private onTeamChanged = (option: Option) => { - const trigger = this.props.trigger; - if (trigger.team === option.value) { - return; - } - delete trigger.job; - delete trigger.project; - delete trigger.jobName; - trigger.team = option.value; - this.props.fieldUpdated(); - this.fetchAvailablePipelines(); - }; - - private onPipelineChanged = (option: Option) => { - const trigger = this.props.trigger; - if (trigger.project && trigger.project.split('/').pop() === option.value) { - return; - } - delete trigger.job; - delete trigger.jobName; - trigger.project = `${trigger.team}/${option.value}`; - this.props.fieldUpdated(); - this.fetchAvailableJobs(); - }; - - private onJobChanged = (option: Option) => { - const trigger = this.props.trigger; - if (trigger.jobName === option.value) { - return; - } - trigger.jobName = option.value; - trigger.job = `${trigger.project}/${trigger.jobName}`; - this.props.fieldUpdated(); - this.fetchAvailableJobs(); - }; - - private onMasterChanged = (option: Option) => { - const trigger = this.props.trigger; - if (trigger.master === option.value) { - return; - } - trigger.master = option.value; - this.props.fieldUpdated(); - this.fetchAvailableTeams(); }; - private fetchAvailableTeams = () => { - const { master } = this.props.trigger; - if (master) { - ConcourseService.listTeamsForMaster(master).then(teams => this.setState({ teams: teams })); - } - }; - - private fetchAvailablePipelines = () => { - const { master, team } = this.props.trigger; - if (master && team) { - ConcourseService.listPipelinesForTeam(master, team).then(pipelines => this.setState({ pipelines: pipelines })); - } - }; - - private fetchAvailableJobs = () => { - const { master, team, project } = this.props.trigger; - if (master && team && project) { - const pipeline = project.split('/').pop(); - ConcourseService.listJobsForPipeline(master, team, pipeline).then(jobs => this.setState({ jobs: jobs })); - } - }; + public render() { + const { ConcourseTriggerContents } = this; + return } />; + } } diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerTemplate.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerTemplate.tsx index e4c0b04b13b..72876bc6c53 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerTemplate.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/ConcourseTriggerTemplate.tsx @@ -1,111 +1,13 @@ import * as React from 'react'; + import { Option } from 'react-select'; -import { $q } from 'ngimport'; -import { IPromise } from 'angular'; -import { IBuild, IBuildTrigger } from 'core/domain'; -import { ITriggerTemplateComponentProps } from 'core/pipeline/manualExecution/TriggerTemplate'; -import { IgorService } from 'core/ci'; -import { Spinner } from 'core/widgets/spinners/Spinner'; import { timestamp } from 'core/utils/timeFormatters'; -import { TetheredSelect } from 'core/presentation/TetheredSelect'; - -export interface IConcourseTriggerTemplateState { - builds?: IBuild[]; - buildsLoading?: boolean; - loadError?: boolean; - selectedBuild?: number; -} - -export class ConcourseTriggerTemplate extends React.Component< - ITriggerTemplateComponentProps, - IConcourseTriggerTemplateState -> { - public static formatLabel(trigger: IBuildTrigger): IPromise { - return $q.when(`(Concourse) ${trigger.master}: ${trigger.job}`); - } - - public constructor(props: ITriggerTemplateComponentProps) { - super(props); - this.state = { - builds: [], - buildsLoading: false, - loadError: false, - selectedBuild: 0, - }; - } - - private buildLoadSuccess = (allBuilds: IBuild[]) => { - const newState: Partial = { - buildsLoading: false, - }; - - const trigger = this.props.command.trigger as IBuildTrigger; - newState.builds = allBuilds - .filter(build => !build.building && build.result === 'SUCCESS') - .sort((a, b) => b.number - a.number); - if (newState.builds.length) { - // default to what is supplied by the trigger if possible; otherwise, use the latest - const defaultSelection = newState.builds.find(b => b.number === trigger.buildNumber) || newState.builds[0]; - newState.selectedBuild = defaultSelection.number; - this.updateSelectedBuild(defaultSelection); - } - - this.setState(newState); - }; - - private buildLoadFailure = () => { - this.setState({ - buildsLoading: false, - loadError: true, - }); - }; - - private updateSelectedBuild = (item: any) => { - this.props.command.extraFields.buildNumber = item.number; - this.props.command.triggerInvalid = false; - this.setState({ selectedBuild: item.number }); - }; - - private initialize = () => { - const { command } = this.props; - command.triggerInvalid = true; - const trigger = command.trigger as IBuildTrigger; - - // These fields will be added to the trigger when the form is submitted - command.extraFields = {}; - - this.setState({ - buildsLoading: true, - loadError: false, - }); - - if (trigger.buildNumber) { - this.updateSelectedBuild(trigger.buildInfo); - } - - // do not re-initialize if the trigger has changed to some other type - if (trigger.type !== 'concourse') { - return; - } - - IgorService.listBuildsForJob(trigger.master, trigger.job).then(this.buildLoadSuccess, this.buildLoadFailure); - }; - - public componentDidMount() { - this.initialize(); - } - - public componentWillReceiveProps(nextProps: ITriggerTemplateComponentProps) { - if (nextProps.command !== this.props.command) { - this.initialize(); - } - } - - private handleBuildChanged = (option: Option): void => { - this.updateSelectedBuild({ number: option.number }); - }; +import { BaseBuildTriggerTemplate } from '../baseBuild/BaseBuildTriggerTemplate'; +import { BuildServiceType } from 'core/ci'; +import { ITriggerTemplateComponentProps } from 'core/pipeline/manualExecution/TriggerTemplate'; +export class ConcourseTriggerTemplate extends React.Component { private optionRenderer = (build: Option) => { return ( @@ -116,40 +18,12 @@ export class ConcourseTriggerTemplate extends React.Component< }; public render() { - const { builds, buildsLoading, loadError, selectedBuild } = this.state; - return ( -
- - {buildsLoading && ( -
-
- -
-
- )} - {loadError &&
Error loading builds!
} - {!buildsLoading && ( -
- {builds.length === 0 && ( -
-

No builds found

-
- )} - {builds.length > 0 && ( - - )} -
- )} -
+ ); } } diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourseTrigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourse.trigger.ts similarity index 91% rename from app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourseTrigger.ts rename to app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourse.trigger.ts index c09eb6ac385..0e315f74556 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourseTrigger.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/concourse/concourse.trigger.ts @@ -1,14 +1,14 @@ import { Registry } from 'core/registry'; import { ArtifactTypePatterns } from 'core/artifact'; -import { ConcourseTriggerConfig } from './ConcourseTriggerConfig'; +import { ConcourseTrigger } from './ConcourseTrigger'; import { ConcourseTriggerTemplate } from './ConcourseTriggerTemplate'; Registry.pipeline.registerTrigger({ label: 'Concourse', description: 'Listens to a Concourse job', key: 'concourse', - component: ConcourseTriggerConfig, + component: ConcourseTrigger, manualExecutionComponent: ConcourseTriggerTemplate, excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], validators: [ diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/git/GitTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/git/GitTrigger.tsx new file mode 100644 index 00000000000..f679f01e757 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/git/GitTrigger.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; +import { has } from 'lodash'; + +import { Application } from 'core/application'; +import { BaseTrigger } from 'core/pipeline'; +import { HelpField } from 'core/help'; +import { IGitTrigger } from 'core/domain'; +import { SETTINGS } from 'core/config/settings'; +import { TextInput } from 'core/presentation'; + +export interface IGitTriggerConfigProps { + trigger: IGitTrigger; + application: Application; + triggerUpdated: (trigger: IGitTrigger) => void; +} + +export class GitTrigger extends React.Component { + private gitTriggerTypes = SETTINGS.gitSources || ['stash', 'github', 'bitbucket', 'gitlab']; + private displayText: any = { + bitbucket: { + 'pipeline.config.git.project': 'Team or User', + 'pipeline.config.git.slug': 'Repo name', + project: 'Team or User name, i.e. spinnaker for bitbucket.org/spinnaker/echo', + slug: 'Repository name (not the url), i.e, echo for bitbucket.org/spinnaker/echo', + }, + github: { + 'pipeline.config.git.project': 'Organization or User', + 'pipeline.config.git.slug': 'Project', + project: 'Organization or User name, i.e. spinnaker for github.com/spinnaker/echo', + slug: 'Project name (not the url), i.e, echo for github.com/spinnaker/echo', + }, + gitlab: { + 'pipeline.config.git.project': 'Organization or User', + 'pipeline.config.git.slug': 'Project', + project: 'Organization or User name, i.e. spinnaker for gitlab.com/spinnaker/echo', + slug: 'Project name (not the url), i.e. echo for gitlab.com/spinnaker/echo', + }, + stash: { + 'pipeline.config.git.project': 'Project', + 'pipeline.config.git.slug': 'Repo name', + project: 'Project name, i.e. SPKR for stash.mycorp.com/projects/SPKR/repos/echo', + slug: 'Repository name (not the url), i.e, echo for stash.mycorp.com/projects/SPKR/repos/echo', + }, + }; + + constructor(props: IGitTriggerConfigProps) { + super(props); + this.state = {}; + } + + public componentDidMount = () => { + const trigger = { ...this.props.trigger }; + const { attributes } = this.props.application; + + if (has(attributes, 'repoProjectKey') && !this.props.trigger.source) { + trigger.source = attributes.repoType; + trigger.project = attributes.repoProjectKey; + trigger.slug = attributes.repoSlug; + } + if (this.gitTriggerTypes.length === 1) { + trigger.source = this.gitTriggerTypes[0]; + } + + this.props.triggerUpdated && this.props.triggerUpdated(trigger); + }; + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + private GitTriggerContents = () => { + const { trigger } = this.props; + const { branch, project, secret, slug, source } = trigger; + const displayText = this.displayText[source ? source : 'github']; + + return ( + <> + {this.gitTriggerTypes && this.gitTriggerTypes.length > 1 && ( +
+ +
+ - - -
-
-
- - -
- -
-
-
- -
- -
-
-
-
- Branch - -
-
- -
-
-
-
- Secret - -
-
- -
-
-
- - -
- diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTrigger.tsx new file mode 100644 index 00000000000..1681268e875 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTrigger.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +import { BuildServiceType } from 'core/ci/igor.service'; +import { + BaseBuildTrigger, + IBaseBuildTriggerConfigProps, +} from 'core/pipeline/config/triggers/baseBuild/BaseBuildTrigger'; + +export class JenkinsTrigger extends React.Component { + public render() { + return ; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTriggerTemplate.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTriggerTemplate.tsx index 750a6076c46..7fdff337689 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTriggerTemplate.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/JenkinsTriggerTemplate.tsx @@ -1,156 +1,11 @@ import * as React from 'react'; -import { Option } from 'react-select'; -import { $q } from 'ngimport'; -import { IPromise } from 'angular'; -import { IBuild, IBuildInfo, IBuildTrigger } from 'core/domain'; +import { BaseBuildTriggerTemplate } from '../baseBuild/BaseBuildTriggerTemplate'; +import { BuildServiceType } from 'core/ci'; import { ITriggerTemplateComponentProps } from 'core/pipeline/manualExecution/TriggerTemplate'; -import { IgorService } from 'core/ci'; -import { Spinner } from 'core/widgets/spinners/Spinner'; -import { buildDisplayName } from 'core/pipeline/executionBuild/buildDisplayName.filter'; -import { timestamp } from 'core/utils/timeFormatters'; -import { TetheredSelect } from 'core/presentation/TetheredSelect'; - -export interface IJenkinsTriggerTemplateState { - builds?: IBuild[]; - buildsLoading?: boolean; - loadError?: boolean; - selectedBuild?: number; -} - -export class JenkinsTriggerTemplate extends React.Component< - ITriggerTemplateComponentProps, - IJenkinsTriggerTemplateState -> { - public static formatLabel(trigger: IBuildTrigger): IPromise { - return $q.when(`(Jenkins) ${trigger.master}: ${trigger.job}`); - } - - public constructor(props: ITriggerTemplateComponentProps) { - super(props); - this.state = { - builds: [], - buildsLoading: false, - loadError: false, - selectedBuild: 0, - }; - } - - private buildLoadSuccess = (allBuilds: IBuild[]) => { - const newState: Partial = { - buildsLoading: false, - }; - - const trigger = this.props.command.trigger as IBuildTrigger; - newState.builds = allBuilds - .filter(build => !build.building && build.result === 'SUCCESS') - .sort((a, b) => b.number - a.number); - if (newState.builds.length) { - // default to what is supplied by the trigger if possible; otherwise, use the latest - const defaultSelection = newState.builds.find(b => b.number === trigger.buildNumber) || newState.builds[0]; - newState.selectedBuild = defaultSelection.number; - this.updateSelectedBuild(defaultSelection); - } - - this.setState(newState); - }; - - private buildLoadFailure = () => { - this.setState({ - buildsLoading: false, - loadError: true, - }); - }; - - private updateSelectedBuild = (item: any) => { - this.props.command.extraFields.buildNumber = item.number; - this.props.command.triggerInvalid = false; - this.setState({ selectedBuild: item.number }); - }; - - private initialize = () => { - const { command } = this.props; - command.triggerInvalid = true; - const trigger = command.trigger as IBuildTrigger; - - // These fields will be added to the trigger when the form is submitted - command.extraFields = {}; - - this.setState({ - buildsLoading: true, - loadError: false, - }); - - if (trigger.buildNumber) { - this.updateSelectedBuild(trigger.buildInfo); - } - - // do not re-initialize if the trigger has changed to some other type - if (trigger.type !== 'jenkins') { - return; - } - - IgorService.listBuildsForJob(trigger.master, trigger.job).then(this.buildLoadSuccess, this.buildLoadFailure); - }; - - public componentDidMount() { - this.initialize(); - } - - public componentWillReceiveProps(nextProps: ITriggerTemplateComponentProps) { - if (nextProps.command !== this.props.command) { - this.initialize(); - } - } - - private handleBuildChanged = (option: Option): void => { - this.updateSelectedBuild({ number: option.number }); - }; - - private optionRenderer = (build: Option) => { - return ( - - Build {build.number} - {buildDisplayName(build as IBuildInfo)}({timestamp(build.timestamp)}) - - ); - }; +export class JenkinsTriggerTemplate extends React.Component { public render() { - const { builds, buildsLoading, loadError, selectedBuild } = this.state; - - return ( -
- - {buildsLoading && ( -
-
- -
-
- )} - {loadError &&
Error loading builds!
} - {!buildsLoading && ( -
- {builds.length === 0 && ( -
-

No builds found

-
- )} - {builds.length > 0 && ( - - )} -
- )} -
- ); + return ; } } diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkins.trigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkins.trigger.ts new file mode 100644 index 00000000000..7efbff29044 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkins.trigger.ts @@ -0,0 +1,31 @@ +'use strict'; + +import { JenkinsTrigger } from './JenkinsTrigger'; +import { Registry } from 'core/registry'; + +import { JenkinsTriggerTemplate } from './JenkinsTriggerTemplate'; +import { JenkinsTriggerExecutionStatus } from './JenkinsTriggerExecutionStatus'; + +Registry.pipeline.registerTrigger({ + component: JenkinsTrigger, + description: 'Listens to a Jenkins job', + executionStatusComponent: JenkinsTriggerExecutionStatus, + executionTriggerLabel: () => 'Triggered Build', + key: 'jenkins', + label: 'Jenkins', + manualExecutionComponent: JenkinsTriggerTemplate, + providesVersionForBake: true, + validators: [ + { + type: 'requiredField', + fieldName: 'job', + message: 'Job is a required field on Jenkins triggers.', + }, + { + type: 'serviceAccountAccess', + message: `You do not have access to the service account configured in this pipeline's Jenkins trigger. + You will not be able to save your edits to this pipeline.`, + preventSave: true, + }, + ], +}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.controller.spec.js b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.controller.spec.js deleted file mode 100644 index 57f507e32b1..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.controller.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -import _ from 'lodash'; - -import { IgorService } from 'core/ci'; - -describe('Controller: jenkinsTrigger', function() { - beforeEach(window.module(require('./jenkinsTrigger.module').name)); - - beforeEach( - window.inject(function($controller, $rootScope, $q) { - this.$q = $q; - this.$scope = $rootScope.$new(); - this.initializeController = function(trigger) { - this.controller = $controller('JenkinsTriggerCtrl', { - $scope: this.$scope, - trigger: trigger, - }); - }; - }), - ); - - describe('updateJobsList', function() { - it('gets list of jobs when initialized with a trigger with a master and sets loading states', function() { - var $q = this.$q, - $scope = this.$scope, - jobs = ['some_job', 'some_other_job'], - trigger = { master: 'jenkins', job: 'some_job' }; - - spyOn(IgorService, 'listJobsForMaster').and.returnValue($q.when(jobs)); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['jenkins'])); - this.initializeController(trigger); - expect($scope.viewState.jobsLoaded).toBe(false); - expect($scope.viewState.mastersLoaded).toBe(false); - $scope.$digest(); - - expect($scope.jobs).toBe(jobs); - expect($scope.masters).toEqual(['jenkins']); - expect($scope.viewState.jobsLoaded).toBe(true); - expect($scope.viewState.mastersLoaded).toBe(true); - }); - - it('updates jobs list when master changes, preserving job if present in both masters', function() { - var masterA = { - name: 'masterA', - jobs: ['a', 'b'], - }, - masterB = { - name: 'masterB', - jobs: ['b', 'c'], - }, - trigger = { - master: 'masterA', - job: 'a', - }, - $scope = this.$scope, - $q = this.$q; - - spyOn(IgorService, 'listJobsForMaster').and.callFake(function() { - return $q.when(_.find([masterA, masterB], { name: $scope.trigger.master }).jobs); - }); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['masterA', 'masterB'])); - this.initializeController(trigger); - $scope.$digest(); - - expect($scope.jobs).toBe(masterA.jobs); - - // Change master, job no longer available, trigger job should be removed - trigger.master = 'masterB'; - $scope.$digest(); - expect(trigger.job).toBe(''); - expect($scope.jobs).toBe(masterB.jobs); - - // Select job in both masters; jobs should not change - trigger.job = 'b'; - $scope.$digest(); - expect(trigger.job).toBe('b'); - expect($scope.jobs).toBe(masterB.jobs); - - // Change master, trigger job should remain - trigger.master = 'masterA'; - $scope.$digest(); - expect(trigger.job).toBe('b'); - expect($scope.jobs).toBe(masterA.jobs); - }); - - it('retains current job if no jobs found in master because that is probably a server-side issue', function() { - var trigger = { - master: 'masterA', - job: 'a', - }, - $scope = this.$scope, - $q = this.$q; - - spyOn(IgorService, 'listJobsForMaster').and.callFake(function() { - return $q.when([]); - }); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['masterA'])); - this.initializeController(trigger); - $scope.$digest(); - - expect(trigger.job).toBe('a'); - }); - }); -}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.html b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.html deleted file mode 100644 index f010a20c8f3..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.html +++ /dev/null @@ -1,62 +0,0 @@ -
- -
- - {{$select.selected}} - - - - -
-
- - - -
-
-
- -
-

(Select a master)

-
- - {{$select.selected}} - - -
-
- -
-
-
- - - -
-
-
-
- Property File - -
-
- -
-
- -
- - -
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.module.js b/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.module.js deleted file mode 100644 index 36b91d87b32..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/jenkins/jenkinsTrigger.module.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -const angular = require('angular'); -import { ServiceAccountReader } from 'core/serviceAccount/ServiceAccountReader'; -import { IgorService, BuildServiceType } from 'core/ci/igor.service'; -import { Registry } from 'core/registry'; -import { SETTINGS } from 'core/config/settings'; - -import { JenkinsTriggerTemplate } from './JenkinsTriggerTemplate'; -import { JenkinsTriggerExecutionStatus } from './JenkinsTriggerExecutionStatus'; - -module.exports = angular - .module('spinnaker.core.pipeline.config.trigger.jenkins', [require('../trigger.directive').name]) - .config(function() { - Registry.pipeline.registerTrigger({ - label: 'Jenkins', - description: 'Listens to a Jenkins job', - key: 'jenkins', - controller: 'JenkinsTriggerCtrl', - controllerAs: 'jenkinsTriggerCtrl', - templateUrl: require('./jenkinsTrigger.html'), - manualExecutionComponent: JenkinsTriggerTemplate, - executionStatusComponent: JenkinsTriggerExecutionStatus, - executionTriggerLabel: () => 'Triggered Build', - providesVersionForBake: true, - validators: [ - { - type: 'requiredField', - fieldName: 'job', - message: 'Job is a required field on Jenkins triggers.', - }, - { - type: 'serviceAccountAccess', - message: `You do not have access to the service account configured in this pipeline's Jenkins trigger. - You will not be able to save your edits to this pipeline.`, - preventSave: true, - }, - ], - }); - }) - .controller('JenkinsTriggerCtrl', [ - '$scope', - 'trigger', - function($scope, trigger) { - $scope.trigger = trigger; - this.fiatEnabled = SETTINGS.feature.fiatEnabled; - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.serviceAccounts = accounts || []; - }); - - $scope.viewState = { - mastersLoaded: false, - mastersRefreshing: false, - jobsLoaded: false, - jobsRefreshing: false, - }; - - function initializeMasters() { - IgorService.listMasters(BuildServiceType.Jenkins).then(function(masters) { - $scope.masters = masters; - $scope.viewState.mastersLoaded = true; - $scope.viewState.mastersRefreshing = false; - }); - } - - this.refreshMasters = function() { - $scope.viewState.mastersRefreshing = true; - initializeMasters(); - }; - - this.refreshJobs = function() { - $scope.viewState.jobsRefreshing = true; - updateJobsList(); - }; - - function updateJobsList() { - if ($scope.trigger && $scope.trigger.master) { - $scope.viewState.jobsLoaded = false; - $scope.jobs = []; - IgorService.listJobsForMaster($scope.trigger.master).then(function(jobs) { - $scope.viewState.jobsLoaded = true; - $scope.viewState.jobsRefreshing = false; - $scope.jobs = jobs; - if (jobs.length && !$scope.jobs.includes($scope.trigger.job)) { - $scope.trigger.job = ''; - } - }); - } - } - - initializeMasters(); - - $scope.$watch('trigger.master', updateJobsList); - }, - ]); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/PipelineTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/PipelineTrigger.tsx new file mode 100644 index 00000000000..6e77307520a --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/PipelineTrigger.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; + +import { Observable } from 'rxjs'; + +import { ApplicationReader } from 'core/application/service/ApplicationReader'; +import { BaseTrigger } from 'core/pipeline'; +import { IPipeline, IPipelineTrigger } from 'core/domain'; +import { PipelineConfigService } from 'core/pipeline/config/services/PipelineConfigService'; +import { Checklist } from 'core/forms'; +import { Application } from 'core'; +import { Subject } from 'rxjs/Subject'; + +export interface IPipelineTriggerConfigProps { + status: string[]; + trigger: IPipelineTrigger; + application: Application; + pipelineId: string; + triggerUpdated: (trigger: IPipelineTrigger) => void; +} + +export interface IPipelineTriggerState { + applications: string[]; + pipelines: IPipeline[]; + pipelinesLoaded: boolean; + useDefaultParameters: { [k: string]: boolean }; + userSuppliedParameters: { [k: string]: string }; +} + +export class PipelineTrigger extends React.Component { + private destroy$ = new Subject(); + private statusOptions = ['successful', 'failed', 'canceled']; + + constructor(props: IPipelineTriggerConfigProps) { + super(props); + + this.state = { + applications: [], + pipelines: [], + pipelinesLoaded: false, + useDefaultParameters: {}, + userSuppliedParameters: {}, + }; + } + + public componentDidMount = () => { + Observable.fromPromise(ApplicationReader.listApplications()) + .takeUntil(this.destroy$) + .subscribe( + applications => this.setState({ applications: applications.map(a => a.name).sort() }), + () => this.setState({ applications: [] }), + ); + + const { application } = this.props.trigger; + this.onUpdateTrigger({ + application: application || this.props.application.name, + status: status || [], + }); + + this.init(application); + }; + + private init = (application: string) => { + const { pipelineId, trigger } = this.props; + if (application) { + Observable.fromPromise(PipelineConfigService.getPipelinesForApplication(application)) + .takeUntil(this.destroy$) + .subscribe(pipelines => { + pipelines = pipelines.filter(p => p.id !== pipelineId); + if (!pipelines.find(p => p.id === trigger.pipeline)) { + this.onUpdateTrigger({ pipeline: null }); + } + this.setState({ + pipelines, + pipelinesLoaded: true, + }); + }); + } + }; + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + private PipelineTriggerContents = () => { + const { application, pipeline, status } = this.props.trigger; + const { applications, pipelines, pipelinesLoaded } = this.state; + return ( + <> +
+
+ +
+ ) => this.onUpdateTrigger({ pipeline: option.value })} + options={pipelines.map(p => ({ label: p.id, value: p.id }))} + placeholder={'Select a pipeline...'} + value={pipeline} + /> + )} +
+
+
+ +
+ ) => this.onUpdateTrigger({ status: s })} + /> +
+
+
+ + ); + }; + + public render() { + const { PipelineTriggerContents } = this; + return } />; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipeline.trigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipeline.trigger.ts new file mode 100644 index 00000000000..ffd11c19446 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipeline.trigger.ts @@ -0,0 +1,18 @@ +'use strict'; + +import { ArtifactTypePatterns } from 'core/artifact'; +import { PipelineTrigger } from './PipelineTrigger'; +import { PipelineTriggerTemplate } from './PipelineTriggerTemplate'; +import { ExecutionUserStatus } from 'core/pipeline/status/ExecutionUserStatus'; +import { Registry } from 'core/registry'; + +Registry.pipeline.registerTrigger({ + component: PipelineTrigger, + description: 'Listens to a pipeline execution', + label: 'Pipeline', + key: 'pipeline', + excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], + executionStatusComponent: ExecutionUserStatus, + manualExecutionComponent: PipelineTriggerTemplate, + executionTriggerLabel: () => 'Pipeline', +}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.html b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.html deleted file mode 100644 index 923fd9bac5e..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.html +++ /dev/null @@ -1,77 +0,0 @@ -
-
- -
- - {{$select.selected}} - - -
-
-
-
-
-
- -
-
- - {{$select.selected.name}} - - -
-
-
-
-

Pipeline Parameters

- -
-
- {{parameter.name}} - -
-
- - -
-
- -
-
-
-
- -
- -
-
- -
- - -
-
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.module.js b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.module.js deleted file mode 100644 index 88971444871..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTrigger.module.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -import _ from 'lodash'; -const angular = require('angular'); - -import { ArtifactTypePatterns } from 'core/artifact'; -import { ServiceAccountReader } from 'core/serviceAccount/ServiceAccountReader'; -import { ApplicationReader } from 'core/application/service/ApplicationReader'; -import { PipelineConfigService } from 'core/pipeline/config/services/PipelineConfigService'; -import { PipelineTriggerTemplate } from './PipelineTriggerTemplate'; -import { ExecutionUserStatus } from 'core/pipeline/status/ExecutionUserStatus'; -import { Registry } from 'core/registry'; -import { SETTINGS } from 'core/config/settings'; - -module.exports = angular - .module('spinnaker.core.pipeline.config.trigger.pipeline', [require('../trigger.directive').name]) - .config(function() { - Registry.pipeline.registerTrigger({ - label: 'Pipeline', - description: 'Listens to a pipeline execution', - key: 'pipeline', - controller: 'pipelineTriggerCtrl', - controllerAs: 'pipelineTriggerCtrl', - templateUrl: require('./pipelineTrigger.html'), - manualExecutionComponent: PipelineTriggerTemplate, - executionStatusComponent: ExecutionUserStatus, - excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], - executionTriggerLabel: () => 'Pipeline', - }); - }) - .controller('pipelineTriggerCtrl', [ - '$scope', - 'trigger', - function($scope, trigger) { - $scope.trigger = trigger; - - this.fiatEnabled = SETTINGS.feature.fiatEnabled; - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.serviceAccounts = accounts || []; - }); - - if (!$scope.trigger.application) { - $scope.trigger.application = $scope.application.name; - } - - if (!$scope.trigger.status) { - $scope.trigger.status = []; - } - - $scope.statusOptions = ['successful', 'failed', 'canceled']; - - function init() { - if ($scope.trigger.application) { - PipelineConfigService.getPipelinesForApplication($scope.trigger.application).then(function(pipelines) { - $scope.pipelines = _.filter(pipelines, function(pipeline) { - return pipeline.id !== $scope.pipeline.id; - }); - if ( - !_.find(pipelines, function(pipeline) { - return pipeline.id === $scope.trigger.pipeline; - }) - ) { - $scope.trigger.pipeline = null; - } - $scope.viewState.pipelinesLoaded = true; - }); - } - } - - $scope.viewState = { - pipelinesLoaded: false, - infiniteScroll: { - numToAdd: 20, - currentItems: 20, - }, - }; - - this.addMoreItems = function() { - $scope.viewState.infiniteScroll.currentItems += $scope.viewState.infiniteScroll.numToAdd; - }; - - ApplicationReader.listApplications().then(function(applications) { - $scope.applications = _.map(applications, 'name').sort(); - }); - - $scope.useDefaultParameters = {}; - $scope.userSuppliedParameters = {}; - - this.updateParam = function(parameter) { - if ($scope.useDefaultParameters[parameter] === true) { - delete $scope.userSuppliedParameters[parameter]; - delete $scope.trigger.parameters[parameter]; - } else if ($scope.userSuppliedParameters[parameter]) { - $scope.trigger.pipelineParameters[parameter] = $scope.userSuppliedParameters[parameter]; - } - }; - - init(); - - $scope.$watch('trigger.application', init); - }, - ]); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTriggerExecutionHandler.spec.js b/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTriggerExecutionHandler.spec.js deleted file mode 100644 index a9c60eecf32..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/pipeline/pipelineTriggerExecutionHandler.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import { PipelineConfigService } from 'core/pipeline/config/services/PipelineConfigService'; -import { PipelineTriggerTemplate } from './PipelineTriggerTemplate'; - -describe('Pipeline Trigger: ExecutionHandler', function() { - var $scope, $q; - - beforeEach(window.module(require('./pipelineTrigger.module').name)); - - beforeEach( - window.inject(function($rootScope, _$q_) { - $scope = $rootScope.$new(); - $q = _$q_; - }), - ); - - it('gets pipeline name from configs', function() { - let label = null; - spyOn(PipelineConfigService, 'getPipelinesForApplication').and.returnValue( - $q.when([{ id: 'b', name: 'expected' }, { id: 'a', name: 'other' }]), - ); - - PipelineTriggerTemplate.formatLabel({ application: 'a', pipeline: 'b' }).then(result => (label = result)); - $scope.$digest(); - expect(label).toBe('(Pipeline) a: expected'); - }); - - it('returns error message if pipeline config is not found', function() { - let label = null; - spyOn(PipelineConfigService, 'getPipelinesForApplication').and.returnValue($q.when([{ id: 'a', name: 'other' }])); - - PipelineTriggerTemplate.formatLabel({ application: 'a', pipeline: 'b' }).then(result => (label = result)); - $scope.$digest(); - expect(label).toBe('[pipeline not found]'); - }); - - it('returns error message if pipelines cannot be loaded', function() { - let label = null; - spyOn(PipelineConfigService, 'getPipelinesForApplication').and.returnValue($q.reject('')); - - PipelineTriggerTemplate.formatLabel({ application: 'a', pipeline: 'b' }).then(result => (label = result)); - $scope.$digest(); - expect(label).toBe(`[could not load pipelines for 'a']`); - }); -}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/PubsubTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/PubsubTrigger.tsx new file mode 100644 index 00000000000..be84d21a12d --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/PubsubTrigger.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; + +import { Observable, Subject } from 'rxjs'; + +import { BaseTrigger } from 'core/pipeline'; +import { HelpField } from 'core/help'; +import { IPubsubSubscription, IPubsubTrigger } from 'core/domain'; +import { MapEditor } from 'core/forms'; +import { PubsubSubscriptionReader } from 'core/pubsub'; +import { Spinner } from 'core/widgets'; +import { SETTINGS } from 'core/config/settings'; + +export interface IPubsubTriggerProps { + trigger: IPubsubTrigger; + triggerUpdated: (trigger: IPubsubTrigger) => void; +} + +export interface IPubsubTriggerState { + pubsubSubscriptions: IPubsubSubscription[]; + subscriptionsLoaded: boolean; +} + +export class PubsubTrigger extends React.Component { + private destroy$ = new Subject(); + private pubsubSystems = SETTINGS.pubsubProviders || ['google']; // TODO(joonlim): Add amazon once it is confirmed that amazon pub/sub works. + + constructor(props: IPubsubTriggerProps) { + super(props); + this.state = { + pubsubSubscriptions: [], + subscriptionsLoaded: false, + }; + } + + public componentDidMount(): void { + Observable.fromPromise(PubsubSubscriptionReader.getPubsubSubscriptions()) + .takeUntil(this.destroy$) + .subscribe( + pubsubSubscriptions => { + this.setState({ + pubsubSubscriptions, + subscriptionsLoaded: true, + }); + }, + () => { + this.setState({ + pubsubSubscriptions: [], + subscriptionsLoaded: true, + }); + }, + ); + } + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + public PubSubTriggerContents() { + const { pubsubSubscriptions, subscriptionsLoaded } = this.state; + const { trigger } = this.props; + const a = trigger.attributeConstraints || {}; + const p = trigger.payloadConstraints || {}; + const filteredPubsubSubscriptions = pubsubSubscriptions + .filter(subscription => subscription.pubsubSystem === trigger.pubsubSystem) + .map(subscription => subscription.subscriptionName); + + if (subscriptionsLoaded) { + return ( + <> +
+ +
+ ) => this.onUpdateTrigger({ subscriptionName: option.value })} + options={filteredPubsubSubscriptions.map(sub => ({ label: sub, value: sub }))} + placeholder="Select Pub/Sub Subscription" + value={trigger.subscriptionName} + /> +
+
+ +
+ +
+
+ Payload Constraints + +
+
+ this.onUpdateTrigger({ payloadConstraints })} + /> +
+
+ +
+
+ Attribute Constraints + +
+
+ this.onUpdateTrigger({ attributeConstraints })} + /> +
+
+ + ); + } else { + return ( +
+ +
+ ); + } + } + + public render() { + const { PubSubTriggerContents } = this; + return } />; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsub.trigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsub.trigger.ts index 757925ec69f..5dcad73df4d 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsub.trigger.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsub.trigger.ts @@ -1,58 +1,12 @@ -import { IController, module } from 'angular'; - import { ArtifactTypePatterns } from 'core/artifact'; -import { IPubsubSubscription, IPubsubTrigger } from 'core/domain'; -import { PubsubSubscriptionReader } from 'core/pubsub'; +import { PubsubTrigger } from './PubsubTrigger'; import { Registry } from 'core/registry'; -import { ServiceAccountReader } from 'core/serviceAccount'; -import { SETTINGS } from 'core/config/settings'; - -class PubsubTriggerController implements IController { - public pubsubSystems = SETTINGS.pubsubProviders || ['google']; // TODO(joonlim): Add amazon once it is confirmed that amazon pub/sub works. - private pubsubSubscriptions: IPubsubSubscription[]; - public filteredPubsubSubscriptions: string[]; - public subscriptionsLoaded = false; - public serviceAccounts: string[]; - - public static $inject = ['trigger']; - constructor(public trigger: IPubsubTrigger) { - this.subscriptionsLoaded = false; - this.refreshPubsubSubscriptions(); - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.serviceAccounts = accounts || []; - }); - } - - // If we ever need a refresh button in pubsubTrigger.html, call this function. - public refreshPubsubSubscriptions(): void { - PubsubSubscriptionReader.getPubsubSubscriptions() - .then(subscriptions => (this.pubsubSubscriptions = subscriptions)) - .catch(() => (this.pubsubSubscriptions = [])) - .finally(() => { - this.subscriptionsLoaded = true; - this.updateFilteredPubsubSubscriptions(); - }); - } - - public updateFilteredPubsubSubscriptions(): void { - this.filteredPubsubSubscriptions = this.pubsubSubscriptions - .filter(subscription => subscription.pubsubSystem === this.trigger.pubsubSystem) - .map(subscription => subscription.subscriptionName); - } -} -export const PUBSUB_TRIGGER = 'spinnaker.core.pipeline.trigger.pubsub'; -module(PUBSUB_TRIGGER, []) - .config(() => { - Registry.pipeline.registerTrigger({ - label: 'Pub/Sub', - description: 'Executes the pipeline when a pubsub message is received', - key: 'pubsub', - controller: 'PubsubTriggerCtrl', - controllerAs: 'vm', - templateUrl: require('./pubsubTrigger.html'), - excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], - validators: [], - }); - }) - .controller('PubsubTriggerCtrl', PubsubTriggerController); +Registry.pipeline.registerTrigger({ + component: PubsubTrigger, + description: 'Executes the pipeline when a pubsub message is received', + excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], + key: 'pubsub', + label: 'Pub/Sub', + validators: [], +}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsubTrigger.html b/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsubTrigger.html deleted file mode 100644 index f828fb0dbfa..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/pubsub/pubsubTrigger.html +++ /dev/null @@ -1,65 +0,0 @@ - -
- -
- -
-
- -
-
- Subscription Name -
-
- -
-
- -
- -
-
- Payload Constraints - -
-
- -
-
- -
-
- Attribute Constraints - -
-
- -
-
- - -
- - -
-
- -
- -
-
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTrigger.tsx new file mode 100644 index 00000000000..b0fc6d8686a --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTrigger.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; + +import { BuildServiceType } from 'core/ci/igor.service'; +import { BaseBuildTrigger, IBaseBuildTriggerConfigProps } from '../baseBuild/BaseBuildTrigger'; + +export class TravisTrigger extends React.Component { + public render() { + return ; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTriggerTemplate.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTriggerTemplate.tsx index 7d7196ede31..da913b25e9e 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTriggerTemplate.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/travis/TravisTriggerTemplate.tsx @@ -1,156 +1,11 @@ import * as React from 'react'; -import { Option } from 'react-select'; -import { $q } from 'ngimport'; -import { IPromise } from 'angular'; -import { IBuild, IBuildInfo, IBuildTrigger } from 'core/domain'; +import { BaseBuildTriggerTemplate } from '../baseBuild/BaseBuildTriggerTemplate'; +import { BuildServiceType } from 'core/ci'; import { ITriggerTemplateComponentProps } from 'core/pipeline/manualExecution/TriggerTemplate'; -import { IgorService } from 'core/ci'; -import { Spinner } from 'core/widgets/spinners/Spinner'; -import { buildDisplayName } from 'core/pipeline/executionBuild/buildDisplayName.filter'; -import { timestamp } from 'core/utils/timeFormatters'; -import { TetheredSelect } from 'core/presentation/TetheredSelect'; - -export interface ITravisTriggerTemplateState { - builds?: IBuild[]; - buildsLoading?: boolean; - loadError?: boolean; - selectedBuild?: number; -} - -export class TravisTriggerTemplate extends React.Component< - ITriggerTemplateComponentProps, - ITravisTriggerTemplateState -> { - public static formatLabel(trigger: IBuildTrigger): IPromise { - return $q.when(`(Travis) ${trigger.master}: ${trigger.job}`); - } - - public constructor(props: ITriggerTemplateComponentProps) { - super(props); - this.state = { - builds: [], - buildsLoading: false, - loadError: false, - selectedBuild: 0, - }; - } - - private buildLoadSuccess = (allBuilds: IBuild[]) => { - const newState: Partial = { - buildsLoading: false, - }; - - const trigger = this.props.command.trigger as IBuildTrigger; - newState.builds = allBuilds - .filter(build => !build.building && build.result === 'SUCCESS') - .sort((a, b) => b.number - a.number); - if (newState.builds.length) { - // default to what is supplied by the trigger if possible; otherwise, use the latest - const defaultSelection = newState.builds.find(b => b.number === trigger.buildNumber) || newState.builds[0]; - newState.selectedBuild = defaultSelection.number; - this.updateSelectedBuild(defaultSelection); - } - - this.setState(newState); - }; - - private buildLoadFailure = () => { - this.setState({ - buildsLoading: false, - loadError: true, - }); - }; - - private updateSelectedBuild = (item: any) => { - this.props.command.extraFields.buildNumber = item.number; - this.props.command.triggerInvalid = false; - this.setState({ selectedBuild: item.number }); - }; - - private initialize = () => { - const { command } = this.props; - command.triggerInvalid = true; - const trigger = command.trigger as IBuildTrigger; - - // These fields will be added to the trigger when the form is submitted - command.extraFields = {}; - - this.setState({ - buildsLoading: true, - loadError: false, - }); - - if (trigger.buildNumber) { - this.updateSelectedBuild(trigger.buildInfo); - } - - // do not re-initialize if the trigger has changed to some other type - if (trigger.type !== 'travis') { - return; - } - - IgorService.listBuildsForJob(trigger.master, trigger.job).then(this.buildLoadSuccess, this.buildLoadFailure); - }; - - public componentWillReceiveProps(nextProps: ITriggerTemplateComponentProps) { - if (nextProps.command !== this.props.command) { - this.initialize(); - } - } - - public componentDidMount() { - this.initialize(); - } - - private handleBuildChanged = (option: Option): void => { - this.updateSelectedBuild({ number: option.number }); - }; - - private optionRenderer = (build: Option) => { - return ( - - Build {build.number} - {buildDisplayName(build as IBuildInfo)}({timestamp(build.timestamp)}) - - ); - }; +export class TravisTriggerTemplate extends React.Component { public render() { - const { builds, buildsLoading, loadError, selectedBuild } = this.state; - - return ( -
- - {buildsLoading && ( -
-
- -
-
- )} - {loadError &&
Error loading builds!
} - {!buildsLoading && ( -
- {builds.length === 0 && ( -
-

No builds found

-
- )} - {builds.length > 0 && ( - - )} -
- )} -
- ); + return ; } } diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travis.trigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/travis/travis.trigger.ts new file mode 100644 index 00000000000..0ae561c4e85 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/travis/travis.trigger.ts @@ -0,0 +1,28 @@ +import { ArtifactTypePatterns } from 'core/artifact'; +import { Registry } from 'core/registry'; + +import { TravisTrigger } from './TravisTrigger'; +import { TravisTriggerTemplate } from './TravisTriggerTemplate'; + +Registry.pipeline.registerTrigger({ + component: TravisTrigger, + description: 'Listens to a Travis job', + excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], + key: 'travis', + label: 'Travis', + manualExecutionComponent: TravisTriggerTemplate, + providesVersionForBake: true, + validators: [ + { + type: 'requiredField', + fieldName: 'job', + message: 'Job is a required field on Travis triggers.', + }, + { + type: 'serviceAccountAccess', + message: `You do not have access to the service account configured in this pipeline's Travis trigger. + You will not be able to save your edits to this pipeline.`, + preventSave: true, + }, + ], +}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.controller.spec.ts b/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.controller.spec.ts deleted file mode 100644 index c844df3393b..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.controller.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { mock, IScope, IQService, IControllerService, IRootScopeService } from 'angular'; -import { find } from 'lodash'; - -import { IgorService } from 'core/ci/igor.service'; -import { IBuildTrigger } from 'core/domain/ITrigger'; -import { TRAVIS_TRIGGER, TravisTrigger } from './travisTrigger.module'; - -describe('Controller: travisTrigger', () => { - let $scope: IScope, $q: IQService, $ctrl: IControllerService; - - beforeEach(mock.module(TRAVIS_TRIGGER)); - - beforeEach( - mock.inject(($controller: IControllerService, $rootScope: IRootScopeService, _$q_: IQService) => { - $ctrl = $controller; - $q = _$q_; - $scope = $rootScope.$new(); - }), - ); - - const initializeController = (trigger: IBuildTrigger): TravisTrigger => { - return $ctrl(TravisTrigger, { - trigger, - $scope, - }); - }; - - describe('updateJobsList', () => { - it('gets list of jobs when initialized with a trigger with a master and sets loading states', () => { - const jobs = ['some_job', 'some_other_job'], - trigger = { master: 'travis', job: 'some_job' } as IBuildTrigger; - - spyOn(IgorService, 'listJobsForMaster').and.returnValue($q.when(jobs)); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['travis'])); - const controller = initializeController(trigger); - expect(controller.viewState.jobsLoaded).toBe(false); - expect(controller.viewState.mastersLoaded).toBe(false); - $scope.$digest(); - expect(controller.jobs).toBe(jobs); - expect(controller.masters).toEqual(['travis']); - expect(controller.viewState.jobsLoaded).toBe(true); - expect(controller.viewState.mastersLoaded).toBe(true); - }); - - it('updates jobs list when master changes, preserving job if present in both masters', () => { - const masterA = { - name: 'masterA', - jobs: ['a', 'b'], - }, - masterB = { - name: 'masterB', - jobs: ['b', 'c'], - }, - trigger = { - master: 'masterA', - job: 'a', - } as IBuildTrigger; - - spyOn(IgorService, 'listJobsForMaster').and.callFake((master: string) => { - return $q.when(find([masterA, masterB], { name: master }).jobs); - }); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['masterA', 'masterB'])); - - const controller = initializeController(trigger); - $scope.$digest(); - - expect(controller.jobs).toBe(masterA.jobs); - - // Change master, job no longer available, trigger job should be removed - trigger.master = 'masterB'; - $scope.$digest(); - expect(trigger.job).toBe(''); - expect(controller.jobs).toBe(masterB.jobs); - - // Select job in both masters; jobs should not change - trigger.job = 'b'; - $scope.$digest(); - expect(trigger.job).toBe('b'); - expect(controller.jobs).toBe(masterB.jobs); - - // Change master, trigger job should remain - trigger.master = 'masterA'; - $scope.$digest(); - expect(trigger.job).toBe('b'); - expect(controller.jobs).toBe(masterA.jobs); - }); - - it('retains current job if no jobs found in master because that is probably a server-side issue', () => { - const trigger = { - master: 'masterA', - job: 'a', - } as IBuildTrigger; - - spyOn(IgorService, 'listJobsForMaster').and.callFake(() => { - return $q.when([]); - }); - spyOn(IgorService, 'listMasters').and.returnValue($q.when(['masterA'])); - initializeController(trigger); - $scope.$digest(); - - expect(trigger.job).toBe('a'); - }); - }); -}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.html b/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.html deleted file mode 100644 index 6efc3e2367e..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.html +++ /dev/null @@ -1,66 +0,0 @@ -
- -
- - {{$select.selected}} - - - - -
-
- - - -
-
-
- -
-

(Select a master)

-
- - {{$select.selected}} - - -
-
- -
-
-
- - - -
-
-
-
- Property File - -
-
- -
-
-
- - -
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.module.ts b/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.module.ts deleted file mode 100644 index 7c03cec332d..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/travis/travisTrigger.module.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { IController, IScope, module } from 'angular'; - -import { ArtifactTypePatterns } from 'core/artifact'; -import { IgorService, BuildServiceType } from 'core/ci/igor.service'; -import { Registry } from 'core/registry'; -import { ServiceAccountReader } from 'core/serviceAccount/ServiceAccountReader'; -import { IBuildTrigger } from 'core/domain/ITrigger'; - -import { SETTINGS } from 'core/config/settings'; -import { TravisTriggerTemplate } from './TravisTriggerTemplate'; - -export interface ITravisTriggerViewState { - mastersLoaded: boolean; - mastersRefreshing: boolean; - jobsLoaded: boolean; - jobsRefreshing: boolean; -} - -export class TravisTrigger implements IController { - public viewState: ITravisTriggerViewState; - public masters: string[]; - public jobs: string[]; - public filterLimit = 100; - private filterThreshold = 500; - public fiatEnabled: boolean; - public serviceAccounts: string[]; - - public static $inject = ['$scope', 'trigger']; - constructor($scope: IScope, public trigger: IBuildTrigger) { - this.fiatEnabled = SETTINGS.feature.fiatEnabled; - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.serviceAccounts = accounts || []; - }); - this.viewState = { - mastersLoaded: false, - mastersRefreshing: false, - jobsLoaded: false, - jobsRefreshing: false, - }; - this.initializeMasters(); - $scope.$watch(() => trigger.master, () => this.updateJobsList()); - } - - public refreshMasters(): void { - this.viewState.mastersRefreshing = true; - this.initializeMasters(); - } - - public refreshJobs(): void { - this.viewState.jobsRefreshing = true; - this.updateJobsList(); - } - - public shouldFilter(): boolean { - return this.jobs && this.jobs.length >= this.filterThreshold; - } - - private initializeMasters(): void { - IgorService.listMasters(BuildServiceType.Travis).then((masters: string[]) => { - this.masters = masters; - this.viewState.mastersLoaded = true; - this.viewState.mastersRefreshing = false; - }); - } - - private updateJobsList(): void { - if (this.trigger && this.trigger.master) { - this.viewState.jobsLoaded = false; - this.jobs = []; - IgorService.listJobsForMaster(this.trigger.master).then(jobs => { - this.viewState.jobsLoaded = true; - this.viewState.jobsRefreshing = false; - this.jobs = jobs; - if (jobs.length && !this.jobs.includes(this.trigger.job)) { - this.trigger.job = ''; - } - }); - } - } -} - -export const TRAVIS_TRIGGER = 'spinnaker.core.pipeline.config.trigger.travis'; -module(TRAVIS_TRIGGER, [require('../trigger.directive').name]) - .config(() => { - Registry.pipeline.registerTrigger({ - label: 'Travis', - description: 'Listens to a Travis job', - key: 'travis', - controller: 'TravisTriggerCtrl', - controllerAs: '$ctrl', - templateUrl: require('./travisTrigger.html'), - manualExecutionComponent: TravisTriggerTemplate, - providesVersionForBake: true, - excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], - validators: [ - { - type: 'requiredField', - fieldName: 'job', - message: 'Job is a required field on Travis triggers.', - }, - { - type: 'serviceAccountAccess', - message: `You do not have access to the service account configured in this pipeline's Travis trigger. - You will not be able to save your edits to this pipeline.`, - preventSave: true, - }, - ], - }); - }) - .controller('TravisTriggerCtrl', TravisTrigger); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/trigger.directive.js b/app/scripts/modules/core/src/pipeline/config/triggers/trigger.directive.js index a37ca41b82a..17d4cde4380 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/trigger.directive.js +++ b/app/scripts/modules/core/src/pipeline/config/triggers/trigger.directive.js @@ -124,11 +124,17 @@ module.exports = angular ReactDOM.render(React.createElement(TriggerConfig, props), triggerBodyNode); const props = { - fieldUpdated: () => { + application: $scope.application, + pipelineId: $scope.pipeline.id, + trigger: $scope.trigger, + triggerUpdated: trigger => { + const triggerIndex = $scope.pipeline.triggers.indexOf($scope.trigger); + trigger = Object.assign(props.trigger, trigger); + $scope.pipeline.triggers[triggerIndex] = trigger; + $scope.trigger = trigger; $scope.fieldUpdated(); renderTrigger(props); }, - trigger: $scope.trigger, }; renderTrigger(props); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/trigger.module.js b/app/scripts/modules/core/src/pipeline/config/triggers/trigger.module.js index 7bfe1ca08d6..dc534cdec95 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/trigger.module.js +++ b/app/scripts/modules/core/src/pipeline/config/triggers/trigger.module.js @@ -3,13 +3,15 @@ const angular = require('angular'); import { RUN_AS_USER_SELECTOR_COMPONENT } from './runAsUserSelector.component'; -import './artifactory/artifactoryTrigger'; -import './concourse/concourseTrigger'; -import { TRAVIS_TRIGGER } from './travis/travisTrigger.module'; -import { WERCKER_TRIGGER } from './wercker/werckerTrigger.module'; -import { GIT_TRIGGER } from './git/git.trigger'; -import { PUBSUB_TRIGGER } from './pubsub/pubsub.trigger'; -import { WEBHOOK_TRIGGER } from './webhook/webhook.trigger'; +import './artifactory/artifactory.trigger'; +import './concourse/concourse.trigger'; +import './git/git.trigger'; +import './jenkins/jenkins.trigger'; +import './pubsub/pubsub.trigger'; +import './pipeline/pipeline.trigger'; +import './travis/travis.trigger'; +import './webhook/webhook.trigger'; +import './wercker/wercker.trigger'; import { ARTIFACT_MODULE } from './artifacts/artifact.module'; import { PIPELINE_ROLES_COMPONENT } from './pipelineRoles.component'; @@ -17,13 +19,6 @@ module.exports = angular.module('spinnaker.core.pipeline.config.trigger', [ ARTIFACT_MODULE, require('../stages/stage.module').name, require('./cron/cronTrigger.module').name, - GIT_TRIGGER, - require('./jenkins/jenkinsTrigger.module').name, - WERCKER_TRIGGER, - TRAVIS_TRIGGER, - require('./pipeline/pipelineTrigger.module').name, - PUBSUB_TRIGGER, - WEBHOOK_TRIGGER, require('./trigger.directive').name, require('./triggers.directive').name, RUN_AS_USER_SELECTOR_COMPONENT, diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/webhook/WebhookTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/webhook/WebhookTrigger.tsx new file mode 100644 index 00000000000..18877d78111 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/webhook/WebhookTrigger.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; + +import { BaseTrigger } from 'core/pipeline'; +import { HelpField } from 'core/help'; +import { MapEditor } from 'core/forms'; +import { IWebhookTrigger } from 'core/domain'; +import { SETTINGS } from 'core/config/settings'; +import { TextInput } from 'core/presentation'; + +export interface IWebhookTriggerProps { + trigger: IWebhookTrigger; + triggerUpdated: (trigger: IWebhookTrigger) => void; +} + +export class WebhookTrigger extends React.Component { + constructor(props: IWebhookTriggerProps) { + super(props); + } + + private WebhookTriggerContents() { + const { trigger } = this.props; + const { source, type } = trigger; + const p = trigger.payloadConstraints || {}; + return ( + <> +
+
+ {`${SETTINGS.gateUrl}/webhooks/${type}/${source || ''}`} +
+
+ +
+
+ Source + +
+
+ ) => + this.onUpdateTrigger({ source: event.target.value }) + } + value={source} + /> +
+
+ +
+
+ Payload Constraints + +
+
+ this.onUpdateTrigger({ payloadConstraints })} + /> +
+
+ + ); + } + + private onUpdateTrigger = (update: any) => { + this.props.triggerUpdated && + this.props.triggerUpdated({ + ...this.props.trigger, + ...update, + }); + }; + + public render() { + const { WebhookTriggerContents } = this; + return } />; + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhook.trigger.ts b/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhook.trigger.ts index c1debf02750..5b515741dc0 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhook.trigger.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhook.trigger.ts @@ -1,47 +1,20 @@ -import { IController, module } from 'angular'; - import { ArtifactTypePatterns } from 'core/artifact'; -import { IWebhookTrigger } from 'core/domain'; import { Registry } from 'core/registry'; -import { ServiceAccountReader } from 'core/serviceAccount/ServiceAccountReader'; -import { SETTINGS } from 'core/config/settings'; - -class WebhookTriggerController implements IController { - public fiatEnabled: boolean; - public serviceAccounts: string[]; - - public static $inject = ['trigger']; - constructor(public trigger: IWebhookTrigger) { - this.fiatEnabled = SETTINGS.feature.fiatEnabled; - ServiceAccountReader.getServiceAccounts().then(accounts => { - this.serviceAccounts = accounts || []; - }); - } - public getTriggerEndpoint(): string { - return `${SETTINGS.gateUrl}/webhooks/${this.trigger.type}/${this.trigger.source || ''}`; - } -} +import { WebhookTrigger } from './WebhookTrigger'; -export const WEBHOOK_TRIGGER = 'spinnaker.core.pipeline.trigger.webhook'; -module(WEBHOOK_TRIGGER, []) - .config(() => { - Registry.pipeline.registerTrigger({ - label: 'Webhook', - description: 'Executes the pipeline when a webhook is received.', - key: 'webhook', - controller: 'WebhookTriggerCtrl', - controllerAs: 'ctrl', - templateUrl: require('./webhookTrigger.html'), - excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], - validators: [ - { - type: 'serviceAccountAccess', - message: `You do not have access to the service account configured in this pipeline's webhook trigger. - You will not be able to save your edits to this pipeline.`, - preventSave: true, - }, - ], - }); - }) - .controller('WebhookTriggerCtrl', WebhookTriggerController); +Registry.pipeline.registerTrigger({ + component: WebhookTrigger, + description: 'Executes the pipeline when a webhook is received.', + excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE], + key: 'webhook', + label: 'Webhook', + validators: [ + { + type: 'serviceAccountAccess', + message: `You do not have access to the service account configured in this pipeline's webhook trigger. + You will not be able to save your edits to this pipeline.`, + preventSave: true, + }, + ], +}); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhookTrigger.html b/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhookTrigger.html deleted file mode 100644 index e120ad9abf2..00000000000 --- a/app/scripts/modules/core/src/pipeline/config/triggers/webhook/webhookTrigger.html +++ /dev/null @@ -1,29 +0,0 @@ - -
-
{{ctrl.getTriggerEndpoint()}}
-
- -
-
- Source - -
-
- -
-
- -
-
- Payload Constraints - -
-
- -
-
-
- - -
-
diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/wercker/WerckerTrigger.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/wercker/WerckerTrigger.tsx new file mode 100644 index 00000000000..4428f5234e4 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/triggers/wercker/WerckerTrigger.tsx @@ -0,0 +1,259 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; +import { Observable, Subject } from 'rxjs'; + +import { Application } from 'core/application'; +import { BaseTrigger } from 'core/pipeline'; +import { BuildServiceType, IgorService } from 'core/ci/igor.service'; +import { IBaseBuildTriggerConfigProps, IBaseBuildTriggerState } from '../baseBuild/BaseBuildTrigger'; +import { IWerckerTrigger } from 'core/domain'; +import { Spinner } from 'core/widgets'; +import { Tooltip } from 'core/presentation'; + +export interface IWerckerTriggerConfigProps extends IBaseBuildTriggerConfigProps { + trigger: IWerckerTrigger; + application: Application; + pipelineId: string; + triggerUpdated: (trigger: IWerckerTrigger) => void; +} + +export interface IWerckerTriggerState extends IBaseBuildTriggerState { + apps: string[]; + pipelines: any[]; +} + +export class WerckerTrigger extends React.Component { + private destroy$ = new Subject(); + + constructor(props: IWerckerTriggerConfigProps) { + super(props); + this.state = { + apps: [], + jobs: [], + jobsLoaded: false, + jobsRefreshing: false, + masters: [], + mastersLoaded: false, + mastersRefreshing: false, + pipelines: [], + }; + } + + public componentDidMount = () => { + this.initializeMasters(); + this.updateJob(this.props.trigger.pipeline); + }; + + private initializeMasters = () => { + Observable.fromPromise(IgorService.listMasters(BuildServiceType.Wercker)) + .takeUntil(this.destroy$) + .subscribe(this.mastersUpdated, () => this.mastersUpdated([])); + }; + + private onMasterUpdated = (option: Option) => { + const master = option.value; + if (this.props.trigger.master !== master) { + this.onUpdateTrigger({ master }); + this.updateJobsList(master); + } + }; + + private refreshMasters = () => { + this.setState({ + mastersRefreshing: true, + }); + this.initializeMasters(); + }; + + private mastersUpdated = (masters: string[]) => { + this.setState({ + masters, + mastersLoaded: true, + mastersRefreshing: false, + }); + if (this.props.trigger.master) { + this.refreshJobs(); + } + }; + + private refreshJobs = () => { + this.setState({ + jobsRefreshing: true, + }); + this.updateJobsList(this.props.trigger.master); + }; + + private jobsUpdated = (jobs: string[]) => { + let { app } = this.props.trigger; + const apps = jobs.map(job => job.substring(job.indexOf('/') + 1, job.lastIndexOf('/'))); + this.setState({ + apps, + jobs, + jobsLoaded: true, + jobsRefreshing: false, + }); + if (!apps.length || !apps.includes(app)) { + app = ''; + this.onUpdateTrigger({ + app, + job: '', + pipeline: '', + }); + } + this.updatePipelinesList(app, jobs); + }; + + private updateJobsList = (master: string) => { + if (master) { + this.setState({ + jobsLoaded: false, + jobs: [], + }); + Observable.fromPromise(IgorService.listJobsForMaster(master)) + .takeUntil(this.destroy$) + .subscribe(this.jobsUpdated, () => this.jobsUpdated([])); + } + }; + + private onAppUpdated = (option: Option) => { + const app = option.value; + if (this.props.trigger.app !== app) { + this.onUpdateTrigger({ app }); + this.updatePipelinesList(app, this.state.jobs); + } + }; + + private onPipelineUpdated = (option: Option) => { + const pipeline = option.value; + if (this.props.trigger.pipeline !== pipeline) { + this.onUpdateTrigger({ pipeline }); + this.updateJob(pipeline); + } + }; + + private updatePipelinesList(app: string, jobs: string[]): void { + const { pipeline } = this.props.trigger; + let pipelines: string[] = []; + + jobs.forEach(a => { + if (app === a.substring(a.indexOf('/') + 1, a.lastIndexOf('/'))) { + pipelines = pipelines.concat(a.substring(a.lastIndexOf('/') + 1)); + } + }); + + this.setState({ pipelines }); + + if (!pipelines.length || (pipeline && !pipelines.includes(pipeline))) { + this.onUpdateTrigger({ + job: '', + pipeline: '', + }); + } + } + + private updateJob(pipeline: string): void { + const { app } = this.props.trigger; + if (app && pipeline) { + this.onUpdateTrigger({ job: app + '/' + pipeline }); + } + } + + private getAppsComponent = (): React.ReactNode => { + const { app } = this.props.trigger; + const { apps, jobsLoaded } = this.state; + return ( + <> + {jobsLoaded && ( + ({ label: m, value: m }))} + placeholder={'Select a master...'} + value={master} + /> + +
+ + + +
+ +
+ +
+ {!master &&

(Select a master)

} + {master && this.getAppsComponent()} +
+
+ {master && ( + + + + )} +
+
+
+ +
+ {!app &&

(Select an application)

} + {app && ( +