-
Notifications
You must be signed in to change notification settings - Fork 900
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloudfoundry/unbind): Delete Service Bindings stage (#9384)
* fix(core): fix state update for pipeline tags * fix(core): make metadata page content overridable * feat(cloudfoundry/unbind): Add Stage in order to Unbind Services * feat(cloudfoundry/unbind): remove restaga and restart from test * feat(cloudfoundry/unbind): fix test * feat(cloudfoundry/unbind): change typeof Observable by observableFrom Co-authored-by: Fernando Freire <fernando.freire@armory.io> Co-authored-by: Zach Smith <33258732+zachsmith1@users.noreply.github.com>
- Loading branch information
1 parent
0524550
commit 45e8b9f
Showing
5 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
...dry/src/pipeline/stages/deleteServiceBindings/CloudFoundryDeleteServiceBindingsConfig.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { FormikErrors } from 'formik'; | ||
import { cloneDeep } from 'lodash'; | ||
import React from 'react'; | ||
|
||
import { FormikStageConfig, FormValidator, IStage, IStageConfigProps } from '@spinnaker/core'; | ||
|
||
import { CloudFoundryDeleteServiceBindingsStageConfigForm } from './CloudFoundryDeleteServiceBindingsStageConfigForm'; | ||
|
||
interface ServiceUnbindingRequests { | ||
serviceInstanceName: String; | ||
} | ||
|
||
export function CloudFoundryDeleteServiceBindingsConfig({ | ||
application, | ||
pipeline, | ||
stage, | ||
updateStage, | ||
}: IStageConfigProps) { | ||
const stageWithDefaults = React.useMemo(() => { | ||
return { | ||
serviceUnbindingRequests: [], | ||
restageRequired: true, | ||
restartRequired: false, | ||
credentials: '', | ||
region: '', | ||
...cloneDeep(stage), | ||
}; | ||
}, []); | ||
|
||
return ( | ||
<FormikStageConfig | ||
application={application} | ||
onChange={updateStage} | ||
pipeline={pipeline} | ||
stage={stageWithDefaults} | ||
validate={validateCloudFoundryDeleteServiceBindingsStage} | ||
render={(props) => <CloudFoundryDeleteServiceBindingsStageConfigForm {...props} />} | ||
/> | ||
); | ||
} | ||
|
||
export function validateCloudFoundryDeleteServiceBindingsStage(stage: IStage): FormikErrors<IStage> { | ||
const formValidator = new FormValidator(stage); | ||
|
||
formValidator.field('credentials', 'Account').required(); | ||
formValidator.field('region', 'Region').required(); | ||
formValidator.field('cluster', 'Cluster').required(); | ||
formValidator.field('target', 'Target').required(); | ||
|
||
formValidator | ||
.field('serviceUnbindingRequests', 'Service Binding Requests') | ||
.required() | ||
.withValidators((serviceUnbindingRequests: ServiceUnbindingRequests[]) => { | ||
if (validateServiceUnbindingRequests(serviceUnbindingRequests)) { | ||
return undefined; | ||
} | ||
return 'There should be at least one service binding request. At a minimum, each request must have a service instance name.'; | ||
}); | ||
|
||
return formValidator.validateForm(); | ||
} | ||
|
||
export function validateServiceUnbindingRequests(serviceUnbindingRequests: ServiceUnbindingRequests[]): boolean { | ||
if (serviceUnbindingRequests?.length < 1) { | ||
return false; | ||
} | ||
return serviceUnbindingRequests.every((req) => req.serviceInstanceName); | ||
} |
167 changes: 167 additions & 0 deletions
167
...ipeline/stages/deleteServiceBindings/CloudFoundryDeleteServiceBindingsStageConfigForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import React from 'react'; | ||
import { from as observableFrom, Subject } from 'rxjs'; | ||
import { takeUntil } from 'rxjs/operators'; | ||
|
||
import { | ||
AccountService, | ||
Application, | ||
IAccount, | ||
IFormikStageConfigInjectedProps, | ||
IRegion, | ||
NgReact, | ||
StageConfigField, | ||
StageConstants, | ||
TextInput, | ||
} from '@spinnaker/core'; | ||
|
||
import { AccountRegionClusterSelector } from '../../../presentation/widgets/accountRegionClusterSelector'; | ||
|
||
interface ICloudfoundryDeleteServiceBindingsStageConfigState { | ||
accounts: IAccount[]; | ||
regions: string[]; | ||
application: Application; | ||
} | ||
|
||
export class CloudFoundryDeleteServiceBindingsStageConfigForm extends React.Component< | ||
IFormikStageConfigInjectedProps, | ||
ICloudfoundryDeleteServiceBindingsStageConfigState | ||
> { | ||
private destroy$ = new Subject(); | ||
|
||
constructor(props: IFormikStageConfigInjectedProps, context: any) { | ||
super(props, context); | ||
this.state = { | ||
accounts: [], | ||
regions: [], | ||
application: props.application, | ||
}; | ||
observableFrom(AccountService.listAccounts('cloudfoundry')) | ||
.pipe(takeUntil(this.destroy$)) | ||
.subscribe((rawAccounts: IAccount[]) => this.setState({ accounts: rawAccounts })); | ||
} | ||
|
||
public componentDidMount() { | ||
const stage = this.props.formik.values; | ||
this.props.formik.setFieldValue('cloudProvider', 'cloudfoundry'); | ||
if (stage.serviceUnbindingRequests && stage.serviceUnbindingRequests.length === 0) { | ||
this.props.formik.setFieldValue('serviceUnbindingRequests', [ | ||
{ | ||
serviceInstanceName: '', | ||
updatable: false, | ||
}, | ||
]); | ||
} | ||
if (stage.credentials) { | ||
this.loadRegions(stage.credentials); | ||
} | ||
} | ||
|
||
public componentWillUnmount(): void { | ||
this.destroy$.next(); | ||
} | ||
|
||
private loadRegions = (creds: string) => { | ||
this.setState({ regions: [] }); | ||
observableFrom(AccountService.getRegionsForAccount(creds)) | ||
.pipe(takeUntil(this.destroy$)) | ||
.subscribe((regionList: IRegion[]) => { | ||
const regions = regionList.map((r) => r.name); | ||
regions.sort((a, b) => a.localeCompare(b)); | ||
this.setState({ regions: regions }); | ||
}); | ||
}; | ||
|
||
private addInputArtifact = () => { | ||
const stage = this.props.formik.values; | ||
const newServiceUnbindingRequests = [ | ||
...stage.serviceUnbindingRequests, | ||
{ | ||
serviceInstanceName: '', | ||
updatable: false, | ||
}, | ||
]; | ||
|
||
this.props.formik.setFieldValue('serviceUnbindingRequests', newServiceUnbindingRequests); | ||
}; | ||
|
||
private removeInputArtifact = (index: number) => { | ||
const stage = this.props.formik.values; | ||
const newServiceUnbindingRequests = [...stage.serviceUnbindingRequests]; | ||
newServiceUnbindingRequests.splice(index, 1); | ||
this.props.formik.setFieldValue('serviceUnbindingRequests', newServiceUnbindingRequests); | ||
}; | ||
|
||
private targetUpdated = (target: string) => { | ||
this.props.formik.setFieldValue('target', target); | ||
}; | ||
|
||
private accountRegionClusterUpdated = (stage: any): void => { | ||
this.props.formik.setFieldValue(`cluster`, stage.cluster); | ||
this.props.formik.setFieldValue(`credentials`, stage.credentials); | ||
this.props.formik.setFieldValue(`region`, stage.region); | ||
}; | ||
|
||
public render() { | ||
const stage = this.props.formik.values; | ||
const { accounts, application } = this.state; | ||
const { target } = stage; | ||
const { TargetSelect } = NgReact; | ||
|
||
return ( | ||
<> | ||
<h4>Basic Settings</h4> | ||
<div className="form-horizontal"> | ||
<AccountRegionClusterSelector | ||
accounts={accounts} | ||
application={application} | ||
cloudProvider={'cloudfoundry'} | ||
isSingleRegion={true} | ||
onComponentUpdate={this.accountRegionClusterUpdated} | ||
component={stage} | ||
/> | ||
</div> | ||
<StageConfigField label="Target"> | ||
<TargetSelect model={{ target }} options={StageConstants.TARGET_LIST} onChange={this.targetUpdated} /> | ||
</StageConfigField> | ||
<h4>Service Unbindings</h4> | ||
{stage.serviceUnbindingRequests && stage.serviceUnbindingRequests.length > 0 && ( | ||
<div className="row form-group"> | ||
{stage.serviceUnbindingRequests.map((a: any, index: number) => { | ||
return ( | ||
<div key={index}> | ||
<div className="col-md-offset-1 col-md-8"> | ||
<StageConfigField label="Service Instance Name"> | ||
<TextInput | ||
onChange={(e: React.ChangeEvent<any>) => { | ||
this.props.formik.setFieldValue( | ||
`serviceUnbindingRequests[${index}].serviceInstanceName`, | ||
e.target.value, | ||
); | ||
}} | ||
value={a.serviceInstanceName} | ||
/> | ||
</StageConfigField> | ||
</div> | ||
<div className="col-md-1"> | ||
<div className="form-control-static"> | ||
<button onClick={() => this.removeInputArtifact(index)}> | ||
<span className="glyphicon glyphicon-trash" /> | ||
<span className="sr-only">Remove field</span> | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
)} | ||
<StageConfigField fieldColumns={8} label={''}> | ||
<button className="btn btn-block btn-sm add-new" onClick={() => this.addInputArtifact()}> | ||
<span className="glyphicon glyphicon-plus-sign" /> | ||
Add Service Unbinding | ||
</button> | ||
</StageConfigField> | ||
</> | ||
); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...undry/src/pipeline/stages/deleteServiceBindings/cloudFoundryDeleteServiceBindingsStage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { ExecutionDetailsTasks, Registry } from '@spinnaker/core'; | ||
|
||
import { | ||
CloudFoundryDeleteServiceBindingsConfig, | ||
validateCloudFoundryDeleteServiceBindingsStage, | ||
} from './CloudFoundryDeleteServiceBindingsConfig'; | ||
|
||
export const CF_DELETE_SERVICE_BINDINGS_STAGE_KEY = 'cloudFoundryDeleteServiceBindings'; | ||
|
||
Registry.pipeline.registerStage({ | ||
label: 'Delete Service Bindings', | ||
description: 'Delete CF service bindings with optional parameters.', | ||
key: CF_DELETE_SERVICE_BINDINGS_STAGE_KEY, | ||
component: CloudFoundryDeleteServiceBindingsConfig, | ||
producesArtifacts: false, | ||
cloudProvider: 'cloudfoundry', | ||
executionDetailsSections: [ExecutionDetailsTasks], | ||
validateFn: validateCloudFoundryDeleteServiceBindingsStage, | ||
}); |
46 changes: 46 additions & 0 deletions
46
...pipeline/stages/deleteServiceBindings/cloudFoundryDeleteServiceBindingsStageForm.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import React from 'react'; | ||
import { mock } from 'angular'; | ||
import { mount } from 'enzyme'; | ||
|
||
import { ApplicationModelBuilder, IStage, REACT_MODULE, SpinFormik, StageConfigField } from '@spinnaker/core'; | ||
import { CloudFoundryDeleteServiceBindingsStageConfigForm } from './CloudFoundryDeleteServiceBindingsStageConfigForm'; | ||
|
||
describe('<CloudFoundryDeleteServiceBindingsStageConfigForm/>', function () { | ||
beforeEach(mock.module(REACT_MODULE)); | ||
beforeEach(mock.inject()); | ||
|
||
const getProps = () => { | ||
return { | ||
application: ApplicationModelBuilder.createApplicationForTests('my-application'), | ||
pipeline: { | ||
application: 'my-application', | ||
id: 'pipeline-id', | ||
limitConcurrent: true, | ||
keepWaitingPipelines: true, | ||
name: 'My Pipeline', | ||
parameterConfig: [], | ||
stages: [], | ||
triggers: [], | ||
}, | ||
} as any; | ||
}; | ||
|
||
it('loads component correctly with 2 serviceUnbindingRequests', function () { | ||
const stage = ({ | ||
serviceUnbindingRequests: [{ serviceInstanceName: 'service1' }, { serviceInstanceName: 'service2' }], | ||
} as unknown) as IStage; | ||
|
||
const props = getProps(); | ||
|
||
const component = mount( | ||
<SpinFormik | ||
initialValues={stage} | ||
onSubmit={() => null} | ||
validate={() => null} | ||
render={(formik) => <CloudFoundryDeleteServiceBindingsStageConfigForm {...props} formik={formik} />} | ||
/>, | ||
); | ||
expect(component.find(StageConfigField).findWhere((x) => x.text() === 'Target').length).toBe(1); | ||
expect(component.find(StageConfigField).findWhere((x) => x.text() === 'Service Instance Name').length).toBe(2); | ||
}); | ||
}); |