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 ( + <> + + + + +