From 5da29652e0f73726ce26de157db4ebff6956f54a Mon Sep 17 00:00:00 2001 From: Jon Schneider Date: Tue, 5 Mar 2019 14:58:07 -0600 Subject: [PATCH] refactor(artifacts): Combine expected artifacts and trigger artifact constraints (#6634) To enable new artifact functional set `feature.artifactsRewrite = true` in Deck. Co-Authored-By: Scott Bloch-Wehba-Seaward --- .../core/src/artifact/ArtifactTypes.ts | 1 + .../artifact/expectedArtifact.service.spec.ts | 23 +- .../src/artifact/expectedArtifact.service.ts | 12 +- .../modules/core/src/artifact/index.ts | 7 + .../react/ArtifactAccountSelector.tsx | 29 +- .../src/artifact/react/ArtifactEditor.tsx | 57 +++ .../artifact/react/ExpectedArtifactEditor.tsx | 96 ++-- .../artifact/react/ExpectedArtifactModal.tsx | 142 ++++++ .../artifact/react/StageArtifactSelector.tsx | 149 +++++++ .../modules/core/src/config/settings.ts | 2 + .../modules/core/src/domain/IArtifact.ts | 5 +- .../core/src/domain/IArtifactEditorProps.ts | 7 +- .../core/src/domain/IArtifactKindConfig.ts | 17 +- .../modules/core/src/help/help.contents.ts | 2 + .../src/pipeline/config/PipelineRegistry.ts | 21 +- .../producesArtifacts/ProducesArtifacts.tsx | 76 ++++ .../src/pipeline/config/stages/stage.html | 7 +- .../pipeline/config/stages/stage.module.js | 8 + .../config/stages/wait/WaitStageConfig.tsx | 2 +- .../triggers/artifacts/ArtifactEditor.ts | 21 + .../TriggerArtifactConstraintSelector.tsx | 110 +++++ .../triggers/artifacts/artifact.component.ts | 5 +- .../triggers/artifacts/artifact.module.ts | 4 +- .../artifacts/base64/Base64ArtifactEditor.tsx | 124 +++++- .../artifacts/base64/base64.artifact.ts | 6 +- .../base64/defaultBase64.artifact.ts | 4 +- .../bitbucket/BitbucketArtifactEditor.tsx | 73 ++- .../artifacts/bitbucket/bitbucket.artifact.ts | 6 +- .../bitbucket/defaultBitbucket.artifact.ts | 4 +- .../artifacts/custom/CustomArtifactEditor.tsx | 82 ---- .../artifacts/custom/custom.artifact.ts | 3 +- .../artifacts/docker/DockerArtifactEditor.tsx | 90 +++- .../docker/defaultDocker.artifact.spec.ts | 2 +- .../docker/defaultDocker.artifact.ts | 5 +- .../artifacts/docker/docker.artifact.ts | 8 +- .../artifacts/gcs/GcsArtifactEditor.tsx | 80 +++- .../artifacts/gcs/defaultGcs.artifact.ts | 4 +- .../triggers/artifacts/gcs/gcs.artifact.ts | 6 +- .../artifacts/github/GithubArtifactEditor.tsx | 88 +++- .../github/defaultGithub.artifact.ts | 4 +- .../artifacts/github/github.artifact.ts | 6 +- .../artifacts/gitlab/GitlabArtifactEditor.tsx | 88 +++- .../gitlab/defaultGitlab.artifact.ts | 4 +- .../artifacts/gitlab/gitlab.artifact.ts | 6 +- .../artifacts/helm/HelmArtifactEditor.tsx | 123 ++--- .../triggers/artifacts/helm/helm.artifact.ts | 9 +- .../artifacts/http/HttpArtifactEditor.tsx | 25 +- .../artifacts/http/defaultHttp.artifact.ts | 38 ++ .../triggers/artifacts/http/http.artifact.ts | 6 +- .../config/triggers/artifacts/index.ts | 43 ++ .../artifacts/ivy/IvyArtifactEditor.tsx | 25 +- .../triggers/artifacts/ivy/ivy.artifact.ts | 6 +- .../kubernetes/KubernetesArtifactEditor.tsx | 78 ++++ .../artifacts/maven/MavenArtifactEditor.tsx | 25 +- .../artifacts/maven/maven.artifact.ts | 6 +- .../artifacts/s3/S3ArtifactEditor.tsx | 42 +- .../artifacts/s3/defaultS3.artifact.ts | 2 + .../triggers/artifacts/s3/s3.artifact.ts | 6 +- .../artifacts/singleFieldArtifactEditor.tsx | 59 +-- .../triggerArtifactConstraintSelector.less | 23 + .../config/triggers/trigger.directive.js | 40 +- .../src/pipeline/config/triggers/trigger.html | 22 +- .../pipeline/config/triggers/triggers.html | 2 +- .../core/src/task/monitor/TaskMonitor.ts | 4 +- .../src/utils/clipboard/CopyToClipboard.tsx | 1 + app/scripts/modules/core/src/utils/index.ts | 1 + app/scripts/modules/core/src/widgets/index.ts | 1 + .../spelText/SpelAutocompleteService.ts | 420 ++++++++++++++++++ .../core/src/widgets/spelText/SpelText.tsx | 76 ++++ .../spelText/spelAutocomplete.service.js | 416 +---------------- .../widgets/spelText/spelText.decorator.js | 4 + .../kubernetes/src/v2/kubernetes.v2.module.ts | 3 +- .../ManifestBindArtifactsSelector.tsx | 84 ++++ .../deployManifestConfig.controller.ts | 65 ++- .../deployManifest/deployManifestConfig.html | 114 +++-- .../deployManifest/deployManifestStage.ts | 3 +- settings.js | 2 + 77 files changed, 2458 insertions(+), 812 deletions(-) create mode 100644 app/scripts/modules/core/src/artifact/react/ArtifactEditor.tsx create mode 100644 app/scripts/modules/core/src/artifact/react/ExpectedArtifactModal.tsx create mode 100644 app/scripts/modules/core/src/artifact/react/StageArtifactSelector.tsx create mode 100644 app/scripts/modules/core/src/pipeline/config/stages/producesArtifacts/ProducesArtifacts.tsx create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/ArtifactEditor.ts create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/TriggerArtifactConstraintSelector.tsx delete mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/custom/CustomArtifactEditor.tsx create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/http/defaultHttp.artifact.ts create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/index.ts create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/kubernetes/KubernetesArtifactEditor.tsx create mode 100644 app/scripts/modules/core/src/pipeline/config/triggers/artifacts/triggerArtifactConstraintSelector.less create mode 100644 app/scripts/modules/core/src/widgets/spelText/SpelAutocompleteService.ts create mode 100644 app/scripts/modules/core/src/widgets/spelText/SpelText.tsx create mode 100644 app/scripts/modules/kubernetes/src/v2/pipelines/stages/deployManifest/ManifestBindArtifactsSelector.tsx diff --git a/app/scripts/modules/core/src/artifact/ArtifactTypes.ts b/app/scripts/modules/core/src/artifact/ArtifactTypes.ts index 97116185e13..102c5c51f17 100644 --- a/app/scripts/modules/core/src/artifact/ArtifactTypes.ts +++ b/app/scripts/modules/core/src/artifact/ArtifactTypes.ts @@ -12,4 +12,5 @@ export const ArtifactTypePatterns = { IVY_FILE: /ivy\/file/, MAVEN_FILE: /maven\/file/, HTTP_FILE: /http\/file/, + FRONT50_PIPELINE_TEMPLATE: /front50\/pipelineTemplate/, }; diff --git a/app/scripts/modules/core/src/artifact/expectedArtifact.service.spec.ts b/app/scripts/modules/core/src/artifact/expectedArtifact.service.spec.ts index b7bb4e7e1e0..8947b359def 100644 --- a/app/scripts/modules/core/src/artifact/expectedArtifact.service.spec.ts +++ b/app/scripts/modules/core/src/artifact/expectedArtifact.service.spec.ts @@ -6,6 +6,7 @@ import { Registry } from 'core/registry'; describe('ExpectedArtifactService', () => { describe('getKindConfig()', () => { const baseKindConfig = { + typePattern: /base-type/, label: '', description: '', isDefault: false, @@ -15,21 +16,25 @@ describe('ExpectedArtifactService', () => { }; const kindConfigs: IArtifactKindConfig[] = [ { + typePattern: /foo-type/, type: 'foo-type', key: 'foo', isMatch: true, }, { + typePattern: /foo-type/, type: 'foo-type', key: 'foo-default', isDefault: true, }, { + typePattern: /bar-type/, type: 'bar-type', key: 'bar', isMatch: true, }, { + typePattern: /bar-type/, type: 'bar-type', key: 'bar-default', isDefault: true, @@ -47,24 +52,6 @@ describe('ExpectedArtifactService', () => { Registry.pipeline.registerCustomArtifactKind(customKindConfig); }); - it('returns the custom kind when set as the artifact kind', () => { - const artifact: IArtifact = { - kind: 'custom', - id: 'artifact-id', - }; - const kindConfig = ExpectedArtifactService.getKindConfig(artifact, false); - expect(kindConfig).toEqual(customKindConfig); - }); - - it('returns the custom kind when set as the artifact kind and isDefault is true', () => { - const artifact: IArtifact = { - kind: 'custom', - id: 'artifact-id', - }; - const kindConfig = ExpectedArtifactService.getKindConfig(artifact, true); - expect(kindConfig).toEqual(customKindConfig); - }); - it('infers kind from type', () => { const artifact: IArtifact = { id: 'artifact-id', diff --git a/app/scripts/modules/core/src/artifact/expectedArtifact.service.ts b/app/scripts/modules/core/src/artifact/expectedArtifact.service.ts index c286e649c62..6b838f76248 100644 --- a/app/scripts/modules/core/src/artifact/expectedArtifact.service.ts +++ b/app/scripts/modules/core/src/artifact/expectedArtifact.service.ts @@ -1,15 +1,13 @@ -/// - import { PipelineConfigService } from 'core/pipeline'; import { Registry } from 'core/registry'; import { - IPipeline, - IStage, - IExpectedArtifact, - IExecutionContext, IArtifact, - IArtifactSource, IArtifactKindConfig, + IArtifactSource, + IExecutionContext, + IExpectedArtifact, + IPipeline, + IStage, } from 'core/domain'; import { UUIDGenerator } from 'core/utils'; import { hri as HumanReadableIds } from 'human-readable-ids'; diff --git a/app/scripts/modules/core/src/artifact/index.ts b/app/scripts/modules/core/src/artifact/index.ts index 678d5ca4bd8..a0c857f0e11 100644 --- a/app/scripts/modules/core/src/artifact/index.ts +++ b/app/scripts/modules/core/src/artifact/index.ts @@ -14,3 +14,10 @@ export * from './NgAppEngineDeployArtifactDelegate'; export * from './NgBakeManifestArtifactDelegate'; export * from './IArtifactAccountPair'; export * from './NgAppengineConfigArtifactDelegate'; +export * from './react/ArtifactAccountSelector'; +export * from './react/ArtifactIcon'; +export * from './react/ExpectedArtifactSelector'; +export * from './react/ExpectedArtifactEditor'; +export * from './react/ExpectedArtifactSourceSelector'; +export * from './react/ExpectedArtifactModal'; +export * from './react/StageArtifactSelector'; diff --git a/app/scripts/modules/core/src/artifact/react/ArtifactAccountSelector.tsx b/app/scripts/modules/core/src/artifact/react/ArtifactAccountSelector.tsx index ecf389ded56..935ffe5b05d 100644 --- a/app/scripts/modules/core/src/artifact/react/ArtifactAccountSelector.tsx +++ b/app/scripts/modules/core/src/artifact/react/ArtifactAccountSelector.tsx @@ -5,37 +5,38 @@ import { react2angular } from 'react2angular'; import { IArtifactAccount } from 'core/account'; import { TetheredSelect } from 'core/presentation'; +import { ArtifactIcon } from './ArtifactIcon'; + export interface IArtifactAccountSelectorProps { accounts: IArtifactAccount[]; selected: IArtifactAccount; - onChange: (_: IArtifactAccount) => void; + onChange: (account: IArtifactAccount) => void; className?: string; } -export interface IArtifactAccountSelectorOption { - value: string; - label: string; -} - export class ArtifactAccountSelector extends React.Component { constructor(props: IArtifactAccountSelectorProps) { super(props); } - private onChange = (option: IArtifactAccountSelectorOption) => { - const account = this.props.accounts.find(a => a.name === option.value); - this.props.onChange(account); + private renderOption = (account: IArtifactAccount) => { + return ( + + + {account.name} + + ); }; public render() { - const options = this.props.accounts.map(a => ({ value: a.name, label: a.name })); - const value = this.props.selected ? { value: this.props.selected.name, label: this.props.selected.name } : null; return ( ); diff --git a/app/scripts/modules/core/src/artifact/react/ArtifactEditor.tsx b/app/scripts/modules/core/src/artifact/react/ArtifactEditor.tsx new file mode 100644 index 00000000000..d7e70469d79 --- /dev/null +++ b/app/scripts/modules/core/src/artifact/react/ArtifactEditor.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { cloneDeep } from 'lodash'; +import { IArtifactAccount } from 'core/account'; +import { ArtifactAccountSelector } from 'core/artifact'; +import { IArtifact, IPipeline } from 'core/domain'; +import { StageConfigField } from 'core/pipeline/config/stages/common'; +import { Registry } from 'core/registry'; +import { UUIDGenerator } from 'core/utils'; + +export interface IArtifactEditorProps { + pipeline: IPipeline; + artifact: IArtifact; + artifactAccounts: IArtifactAccount[]; + onArtifactEdit: (artifact: IArtifact) => void; + isDefault: boolean; +} + +/** + * Editor for either the match or default side of an expected artifact. Also + * used in stages where an inline default artifact may be defined. + */ +export class ArtifactEditor extends React.Component { + private onArtifactAccountChanged = (artifactAccount: IArtifactAccount) => { + // reset artifact fields if the account type has changed, so we don't leave dangling properties + // that are not modifiable using the new artifact account type's form. + const artifact: IArtifact = + this.props.artifact && artifactAccount.types.includes(this.props.artifact.type) + ? cloneDeep(this.props.artifact) + : { id: UUIDGenerator.generateUuid() }; + artifact.artifactAccount = artifactAccount.name; + this.props.onArtifactEdit(artifact); + }; + + public render(): React.ReactNode { + const { pipeline, artifact, artifactAccounts, onArtifactEdit, isDefault } = this.props; + const artifactAccount = artifactAccounts.find(acc => acc.name === artifact.artifactAccount) || artifactAccounts[0]; + const accountTypes = artifactAccount ? artifactAccount.types : undefined; + const kinds = isDefault ? Registry.pipeline.getDefaultArtifactKinds() : Registry.pipeline.getMatchArtifactKinds(); + const kind = accountTypes ? kinds.find(a => accountTypes.some(typ => a.typePattern.test(typ))) : undefined; + const EditCmp = kind && kind.editCmp; + + return ( + <> + + + + {EditCmp && ( + + )} + + ); + } +} diff --git a/app/scripts/modules/core/src/artifact/react/ExpectedArtifactEditor.tsx b/app/scripts/modules/core/src/artifact/react/ExpectedArtifactEditor.tsx index e86ca2d2d6b..efe379a40a2 100644 --- a/app/scripts/modules/core/src/artifact/react/ExpectedArtifactEditor.tsx +++ b/app/scripts/modules/core/src/artifact/react/ExpectedArtifactEditor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { react2angular } from 'react2angular'; import { cloneDeep } from 'lodash'; -import { IExpectedArtifact, IArtifactKindConfig, IArtifact } from 'core/domain'; +import { IExpectedArtifact, IArtifactKindConfig, IArtifact, IPipeline } from 'core/domain'; import { IArtifactAccount } from 'core/account'; import { StageConfigField } from 'core/pipeline'; @@ -24,13 +24,13 @@ export interface IExpectedArtifactEditorProps { kinds: IArtifactKindConfig[]; sources: IExpectedArtifactSourceOption[]; accounts: IArtifactAccount[]; + onArtifactChange?: (_a: IArtifact) => void; onSave: (e: IExpectedArtifactEditorSaveEvent) => void; showIcons?: boolean; showAccounts?: boolean; + hidePipelineFields?: boolean; className?: string; - fieldGroupClassName?: string; - fieldColumns: number; - singleColumn: boolean; + pipeline?: IPipeline; } interface IExpectedArtifactEditorState { @@ -58,10 +58,11 @@ export class ExpectedArtifactEditor extends React.Component< constructor(props: IExpectedArtifactEditorProps) { super(props); + const expectedArtifact = props.default ? cloneDeep(props.default) : ExpectedArtifactService.createEmptyArtifact(); this.state = { - expectedArtifact: props.default ? cloneDeep(props.default) : ExpectedArtifactService.createEmptyArtifact(), + expectedArtifact: expectedArtifact, source: props.sources[0], - account: null, + account: this.accountsForExpectedArtifact(expectedArtifact)[0], }; } @@ -85,13 +86,6 @@ export class ExpectedArtifactEditor extends React.Component< this.setState({ source: e }); }; - public onDisplayNameEdit = (e: React.FormEvent) => { - const newName = e.currentTarget.value; - const expectedArtifact = { ...this.state.expectedArtifact }; - expectedArtifact.displayName = newName; - this.setState({ expectedArtifact }); - }; - private onKindChange = (kind: IArtifactKindConfig) => { const expectedArtifact = cloneDeep(this.state.expectedArtifact); expectedArtifact.matchArtifact.type = kind.type; @@ -103,6 +97,14 @@ export class ExpectedArtifactEditor extends React.Component< }; private onAccountChange = (account: IArtifactAccount) => { + const { expectedArtifact } = this.state; + this.props.onArtifactChange && + this.props.onArtifactChange({ + artifactAccount: account.name, + id: expectedArtifact.matchArtifact.id, + reference: expectedArtifact.matchArtifact.name, + type: expectedArtifact.matchArtifact.type, + }); this.setState({ account }); }; @@ -113,6 +115,13 @@ export class ExpectedArtifactEditor extends React.Component< const accounts = this.accountsForExpectedArtifact(expectedArtifact); account = accounts[0]; } + this.props.onArtifactChange && + this.props.onArtifactChange({ + artifactAccount: account ? account.name : '', + id: artifact.id, + reference: artifact.name, + type: artifact.type, + }); this.setState({ expectedArtifact, account }); }; @@ -126,8 +135,18 @@ export class ExpectedArtifactEditor extends React.Component< } }; + private onArtifactDisplayNameEdit = (event: React.ChangeEvent): void => { + const displayName = event.target.value; + const { expectedArtifact } = this.state; + const newExpectedArtifact = { + ...expectedArtifact, + displayName, + }; + this.setState({ expectedArtifact: newExpectedArtifact }); + }; + public render() { - const { sources, showIcons, showAccounts, fieldColumns, singleColumn, fieldGroupClassName } = this.props; + const { sources, showIcons, showAccounts, hidePipelineFields } = this.props; const { expectedArtifact, source, account } = this.state; const accounts = this.accountsForExpectedArtifact(expectedArtifact); const artifact = ExpectedArtifactService.artifactFromExpected(expectedArtifact); @@ -136,17 +155,21 @@ export class ExpectedArtifactEditor extends React.Component< const EditCmp = kind && kind.editCmp; return ( <> - - - - - - - + {!hidePipelineFields && ( + + + + )} + {sources.length > 1 && ( + + + + )} + {showAccounts && ( - + )} @@ -164,17 +187,16 @@ export class ExpectedArtifactEditor extends React.Component< account={account} artifact={artifact} onChange={this.onArtifactEdit} - labelColumns={3} - fieldColumns={fieldColumns} - singleColumn={singleColumn} - groupClassName={fieldGroupClassName} + pipeline={this.props.pipeline} /> )} - - - + {!hidePipelineFields && ( + + + + )} ); } @@ -195,8 +217,6 @@ module(EXPECTED_ARTIFACT_EDITOR_COMPONENT_REACT, [ 'showIcons', 'showAccounts', 'className', - 'fieldColumns', - 'singleColumn', - 'fieldGroupClassName', + 'pipeline', ]), ); diff --git a/app/scripts/modules/core/src/artifact/react/ExpectedArtifactModal.tsx b/app/scripts/modules/core/src/artifact/react/ExpectedArtifactModal.tsx new file mode 100644 index 00000000000..3ae094ead89 --- /dev/null +++ b/app/scripts/modules/core/src/artifact/react/ExpectedArtifactModal.tsx @@ -0,0 +1,142 @@ +import * as React from 'react'; + +import { AccountService, IArtifactAccount } from 'core/account'; +import { ExpectedArtifactService } from 'core/artifact'; +import { IArtifact, IExpectedArtifact, IPipeline } from 'core/domain'; +import { HelpField } from 'core/help'; +import { WizardModal, WizardPage } from 'core/modal/wizard'; +import { IModalComponentProps, ReactModal } from 'core/presentation'; +import { FormikFormField } from 'core/presentation/forms'; +import { TextInput, CheckboxInput } from 'core/presentation/forms/inputs'; +import { TaskMonitor } from 'core/task'; +import { noop } from 'core/utils'; +import { ArtifactEditor } from './ArtifactEditor'; +import { FormikProps } from 'formik'; + +export interface IExpectedArtifactModalProps extends IModalComponentProps { + expectedArtifact?: IExpectedArtifact; + pipeline: IPipeline; + excludedArtifactTypePatterns?: RegExp[]; +} + +export interface IExpectedArtifactModalState { + taskMonitor: TaskMonitor; + artifactAccounts: IArtifactAccount[]; +} + +export class ExpectedArtifactModal extends React.Component { + constructor(props: IExpectedArtifactModalProps) { + super(props); + + this.state = { + artifactAccounts: [], + taskMonitor: new TaskMonitor({ title: "I'm never used" }), + }; + } + + public static defaultProps: Partial = { + closeModal: noop, + dismissModal: noop, + }; + + public componentDidMount(): void { + const excludedPatterns = this.props.excludedArtifactTypePatterns; + AccountService.getArtifactAccounts().then(artifactAccounts => { + this.setState({ + artifactAccounts: excludedPatterns + ? artifactAccounts.filter( + account => !account.types.some(typ => excludedPatterns.some(typPattern => typPattern.test(typ))), + ) + : artifactAccounts, + }); + }); + } + + public static show(props: IExpectedArtifactModalProps): Promise { + const modalProps = { dialogClassName: 'wizard-modal modal-lg' }; + return ReactModal.show(ExpectedArtifactModal, props, modalProps); + } + + private editArtifact = (formik: FormikProps, field: string, artifact: IArtifact) => { + formik.setFieldValue(field, artifact); + formik.setFieldValue(`${field}.artifactAccount`, artifact.artifactAccount); + }; + + public render(): React.ReactNode { + const { artifactAccounts } = this.state; + return ( + + heading="Expected Artifact" + initialValues={this.props.expectedArtifact || ExpectedArtifactService.createEmptyArtifact()} + taskMonitor={this.state.taskMonitor} + dismissModal={this.props.dismissModal} + closeModal={this.props.closeModal} + submitButtonLabel="Save Artifact" + render={({ formik, nextIdx, wizard }) => ( + <> + ( + } + required={true} + /> + )} + /> + ( + this.editArtifact(formik, 'matchArtifact', artifact)} + isDefault={false} + /> + )} + /> + + ( + <> + } + help={} + /> + } + help={} + /> + {formik.values.useDefaultArtifact && ( + this.editArtifact(formik, 'defaultArtifact', artifact)} + isDefault={true} + /> + )} + + )} + /> + + )} + /> + ); + } +} diff --git a/app/scripts/modules/core/src/artifact/react/StageArtifactSelector.tsx b/app/scripts/modules/core/src/artifact/react/StageArtifactSelector.tsx new file mode 100644 index 00000000000..10f0308b888 --- /dev/null +++ b/app/scripts/modules/core/src/artifact/react/StageArtifactSelector.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { module } from 'angular'; +import Select from 'react-select'; +import { react2angular } from 'react2angular'; + +import { IArtifact, IExpectedArtifact, IPipeline, IStage } from 'core/domain'; +import { ArtifactIcon, ExpectedArtifactService } from 'core/artifact'; +import { AccountService, IArtifactAccount } from 'core/account'; +import { ArtifactEditor } from './ArtifactEditor'; + +export interface IStageArtifactSelectorProps { + pipeline: IPipeline; + stage: IStage; + + // one of these two will be defined by this selector + expectedArtifactId?: string; + artifact?: IArtifact; + + onExpectedArtifactSelected: (expectedArtifact: IExpectedArtifact) => void; + onArtifactEdited: (artifact: IArtifact) => void; + + excludedArtifactIds?: string[]; + excludedArtifactTypePatterns?: RegExp[]; +} + +export interface IStageArtifactSelectorState { + artifactAccounts: IArtifactAccount[]; +} + +const DEFINE_NEW_ARTIFACT = '__inline.artifact__'; + +export class StageArtifactSelector extends React.Component { + private defineNewArtifactOption: IExpectedArtifact = { + ...ExpectedArtifactService.createEmptyArtifact(), + displayName: 'Define a new artifact...', + id: DEFINE_NEW_ARTIFACT, + }; + + constructor(props: IStageArtifactSelectorProps) { + super(props); + + this.state = { + artifactAccounts: [], + }; + } + + public componentDidMount(): void { + const excludedPatterns = this.props.excludedArtifactTypePatterns; + AccountService.getArtifactAccounts().then(artifactAccounts => { + this.setState({ + artifactAccounts: excludedPatterns + ? artifactAccounts.filter( + account => !account.types.some(typ => excludedPatterns.some(typPattern => typPattern.test(typ))), + ) + : artifactAccounts, + }); + }); + } + + private renderArtifact = (value: IExpectedArtifact) => { + return ( + + {value.id !== DEFINE_NEW_ARTIFACT && ( + + )} + {value && value.displayName} + + ); + }; + + private onExpectedArtifactSelected = (value: IExpectedArtifact) => { + if (value.id !== DEFINE_NEW_ARTIFACT) { + this.props.onExpectedArtifactSelected(value); + } else { + this.props.onArtifactEdited(value.defaultArtifact); + } + }; + + private onInlineArtifactChanged = (value: IArtifact) => { + this.props.onArtifactEdited(value); + }; + + public render() { + const { pipeline, stage, expectedArtifactId, artifact, excludedArtifactIds } = this.props; + const expectedArtifacts = ExpectedArtifactService.getExpectedArtifactsAvailableToStage(stage, pipeline); + const expectedArtifact = expectedArtifactId + ? expectedArtifacts.find(a => a.id === expectedArtifactId) + : artifact + ? { + id: DEFINE_NEW_ARTIFACT, + displayName: 'Artifact from execution context', + defaultArtifact: artifact, + } + : undefined; + + const options = [ + this.defineNewArtifactOption, + ...expectedArtifacts.filter( + (a: IExpectedArtifact) => !excludedArtifactIds || !excludedArtifactIds.includes(a.id), + ), + ]; + + return ( + <> +
+ this.handleChange(i, a)} + options={availableArtifacts} + optionRenderer={this.renderArtifact} + valueRenderer={this.renderArtifact} + placeholder="Select or define an artifact..." + /> +
+ + {artifact && ( + + )} + + ); + const renderSelectEditable = (artifact: IExpectedArtifact, i: number) => renderSelect(i, artifact); + + return ( + <> + {selectedAsArtifacts.map(renderSelectEditable)} + {renderSelect(selected.length)} + + ); + } +} + +export const TRIGGER_ARTIFACT_CONSTRAINT_SELECTOR_REACT = 'spinnaker.core.trigger.artifact.selector.react'; +module(TRIGGER_ARTIFACT_CONSTRAINT_SELECTOR_REACT, []).component( + 'triggerArtifactConstraintSelectorReact', + react2angular(TriggerArtifactConstraintSelector, [ + 'pipeline', + 'artifactReferer', + 'selected', + 'onDefineExpectedArtifact', + 'onChangeSelected', + ]), +); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.component.ts b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.component.ts index b01cf0897a1..0be65b8fe06 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.component.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.component.ts @@ -70,9 +70,8 @@ class ArtifactCtrl implements IController { if (this.artifactAccounts) { options = options.filter(o => { const isCustomArtifact = o.customKind; - const isPublic = !!o.isPubliclyAccessible; const hasCredential = this.artifactAccounts.find(a => a.types.includes(o.type)); - return isCustomArtifact || isPublic || hasCredential; + return isCustomArtifact || hasCredential; }); } return options.sort((a, b) => a.label.localeCompare(b.label)); @@ -94,7 +93,7 @@ class ArtifactCtrl implements IController { } public artifactIconPath(kindConfig: IArtifactKindConfig) { - return ArtifactIconService.getPath(kindConfig.type); + return kindConfig && ArtifactIconService.getPath(kindConfig.type); } } diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.module.ts b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.module.ts index ebed3a3d750..b57a1b74aae 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.module.ts +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/artifact.module.ts @@ -1,7 +1,6 @@ import { module } from 'angular'; import { EXPECTED_ARTIFACT } from './expectedArtifact.component'; -import { CUSTOM_ARTIFACT } from './custom/custom.artifact'; import { GCS_ARTIFACT } from './gcs/gcs.artifact'; import { DOCKER_ARTIFACT } from './docker/docker.artifact'; import { DEFAULT_DOCKER_ARTIFACT } from './docker/defaultDocker.artifact'; @@ -9,6 +8,7 @@ import { DEFAULT_GCS_ARTIFACT } from './gcs/defaultGcs.artifact'; import { DEFAULT_GITHUB_ARTIFACT } from './github/defaultGithub.artifact'; import { DEFAULT_GITLAB_ARTIFACT } from './gitlab/defaultGitlab.artifact'; import { DEFAULT_BITBUCKET_ARTIFACT } from './bitbucket/defaultBitbucket.artifact'; +import { DEFAULT_HTTP_ARTIFACT } from './http/defaultHttp.artifact'; import { ARTIFACT } from './artifact.component'; import { GITHUB_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/github/github.artifact'; import { GITLAB_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/gitlab/gitlab.artifact'; @@ -21,6 +21,7 @@ import { DEFAULT_S3_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/s3/ import { IVY_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/ivy/ivy.artifact'; import { MAVEN_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/maven/maven.artifact'; import { HTTP_ARTIFACT } from 'core/pipeline/config/triggers/artifacts/http/http.artifact'; +import { CUSTOM_ARTIFACT } from './custom/custom.artifact'; export const ARTIFACT_MODULE = 'spinnaker.core.pipeline.config.trigger.artifacts'; @@ -45,5 +46,6 @@ module(ARTIFACT_MODULE, [ DEFAULT_GITLAB_ARTIFACT, DEFAULT_BITBUCKET_ARTIFACT, DEFAULT_BASE64_ARTIFACT, + DEFAULT_HTTP_ARTIFACT, ARTIFACT, ]); diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/base64/Base64ArtifactEditor.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/base64/Base64ArtifactEditor.tsx index e5bdcc38e37..3752f5e57a2 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/base64/Base64ArtifactEditor.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/base64/Base64ArtifactEditor.tsx @@ -1,3 +1,125 @@ +import * as React from 'react'; +import { has, cloneDeep } from 'lodash'; + +import { ArtifactTypePatterns } from 'core/artifact'; +import { IArtifactEditorProps, IArtifactKindConfig } from 'core/domain'; +import { StageConfigField } from 'core/pipeline'; +import { CopyToClipboard } from 'core/utils'; +import { SpelText } from 'core/widgets'; + import { singleFieldArtifactEditor } from '../singleFieldArtifactEditor'; +import './base64.artifact.less'; + +const TYPE = 'embedded/base64'; + +interface IDefaultBase64ArtifactEditorState { + decoded: string; + encodeDecodeError: string; +} + +class DefaultBase64ArtifactEditor extends React.Component { + private static DOMBase64Errors: { [key: string]: string } = { + 5: 'The string to encode contains characters outside the latin1 range.', + }; + + constructor(props: IArtifactEditorProps) { + super(props); + if (props.artifact.type !== TYPE) { + const clonedArtifact = cloneDeep(props.artifact); + clonedArtifact.type = TYPE; + props.onChange(clonedArtifact); + } + const [decoded, encodeDecodeError] = this.convert(atob, props.artifact.reference); + this.state = { decoded: decoded, encodeDecodeError: encodeDecodeError }; + } + + private convert = (fn: (s: string) => string, str: string): [string, string] => { + try { + const converted = fn(str); + return [converted, '']; + } catch (e) { + if (has(DefaultBase64ArtifactEditor.DOMBase64Errors, e.code)) { + return ['', DefaultBase64ArtifactEditor.DOMBase64Errors[e.code]]; + } else { + return ['', e.message]; + } + } + }; + + private onNameChanged = (name: string) => { + const artifact = cloneDeep(this.props.artifact); + artifact.name = name; + this.props.onChange(artifact); + }; + + private onContentChanged = (event: React.ChangeEvent) => { + const [encoded, encodeDecodeError] = this.convert(btoa, event.target.value); + if (!encodeDecodeError) { + const artifact = cloneDeep(this.props.artifact); + artifact.reference = encoded; + this.props.onChange(artifact); + } + this.setState({ encodeDecodeError: encodeDecodeError }); + }; + + public render() { + const { pipeline } = this.props; + const { name, reference } = this.props.artifact; + const { decoded, encodeDecodeError } = this.state; + return ( + <> + + + + +