diff --git a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/helm/HelmArtifactEditor.tsx b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/helm/HelmArtifactEditor.tsx index 74f19ccb329..e0838c56507 100644 --- a/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/helm/HelmArtifactEditor.tsx +++ b/app/scripts/modules/core/src/pipeline/config/triggers/artifacts/helm/HelmArtifactEditor.tsx @@ -1,42 +1,68 @@ import * as React from 'react'; import { Option } from 'react-select'; +import { cloneDeep } from 'lodash'; import { ArtifactTypePatterns } from 'core/artifact'; import { IArtifact, IArtifactEditorProps, IArtifactKindConfig } from 'core/domain'; import { StageConfigField } from 'core/pipeline'; -import { TetheredSelect } from 'core/presentation'; - +import { TetheredSelect, TetheredCreatable } from 'core/presentation'; import { ArtifactService } from '../ArtifactService'; -import { cloneDeep } from 'lodash'; +import { Spinner } from 'core/widgets'; const TYPE = 'helm/chart'; interface IHelmArtifactEditorState { names: string[]; - versions: string[]; + versions: Array>; + versionsLoading: boolean; + namesLoading: boolean; } class HelmEditor extends React.Component { + public state: IHelmArtifactEditorState = { + names: [], + versions: [], + versionsLoading: true, + namesLoading: true, + }; + + // taken from https://github.com/semver/semver/issues/232 + private SEMVER: RegExp = new RegExp( + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/, + ); + constructor(props: IArtifactEditorProps) { super(props); - if (props.artifact.type !== TYPE) { - const clonedArtifact = cloneDeep(props.artifact); + const { artifact } = this.props; + if (artifact.type !== TYPE) { + const clonedArtifact = cloneDeep(artifact); clonedArtifact.type = TYPE; props.onChange(clonedArtifact); } - this.state = { - names: [], - versions: [], - }; + ArtifactService.getArtifactNames(TYPE, this.props.account.name).then(names => { - this.setState({ names }); + this.setState({ + names, + namesLoading: false, + }); }); } - public componentWillReceiveProps(nextProps: IArtifactEditorProps) { + public componentDidMount() { + const { artifact } = this.props; + if (artifact.name) { + this.getChartVersionOptions(artifact.name); + } + } + + public componentDidUpdate(nextProps: IArtifactEditorProps) { if (this.props.account.name !== nextProps.account.name) { ArtifactService.getArtifactNames(TYPE, nextProps.account.name).then(names => { - this.setState({ names, versions: [] }); + this.setState({ + names, + namesLoading: false, + versions: [], + }); }); } } @@ -44,31 +70,35 @@ class HelmEditor extends React.Component ({ value: name, label: name })); - const versionOptions = this.state.versions.map(version => ({ value: version, label: version })); + return ( <> - { - this.onChange(e, 'name'); - this.onNameChange(e.value.toString()); - }} - clearable={false} - /> + {!this.state.namesLoading && ( + { + this.onChange(e, 'name'); + this.getChartVersionOptions(e.value.toString()); + }} + clearable={false} + /> + )} + {this.state.namesLoading && } - { - this.onChange(e, 'version'); - }} - clearable={false} - /> + {!this.state.versionsLoading && ( + { + this.onChange(e, 'version'); + }} + clearable={false} + /> + )} + {this.state.versionsLoading && } ); @@ -80,11 +110,21 @@ class HelmEditor extends React.Component { - ArtifactService.getArtifactVersions(TYPE, this.props.account.name, chartName).then(versions => { - this.setState({ versions }); + private getChartVersionOptions(chartName: string) { + const { artifact, account } = this.props; + this.setState({ versionsLoading: true }); + ArtifactService.getArtifactVersions(TYPE, account.name, chartName).then((versions: string[]) => { + // if the version doesn't match SEMVER we assume that it's a regular expression or SpEL expression + // and add it to the list of valid versions + if (artifact.version && !this.SEMVER.test(artifact.version)) { + versions = versions.concat(artifact.version); + } + this.setState({ + versions: versions.map(v => ({ label: v, value: v })), + versionsLoading: false, + }); }); - }; + } } export const HelmMatch: IArtifactKindConfig = { diff --git a/app/scripts/modules/core/src/presentation/TetheredCreatable.tsx b/app/scripts/modules/core/src/presentation/TetheredCreatable.tsx new file mode 100644 index 00000000000..a8619837e37 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/TetheredCreatable.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { Creatable } from 'react-select'; +import * as TetherComponent from 'react-tether'; + +/* from https://github.com/JedWatson/react-select/issues/810#issuecomment-250274937 **/ +export class TetheredCreatable extends Creatable { + constructor(props: any) { + super(props); + (this as any).renderOuter = this._renderOuter; + } + + public _renderOuter() { + // @ts-ignore - type definitions don't expose renderOuter... + const menu = super.render.apply(this, arguments); + + // Don't return an updated menu render if we don't have one + if (!menu) { + return null; + } + + /* this.wrapper comes from the ref of the main Select component (super.render()) **/ + const selectWidth = (this as any).wrapper ? (this as any).wrapper.offsetWidth : null; + + return ( + // @ts-ignore - type definitions don't expose renderOuter... + + {/* Apply position:static to our menu so that its parent will get the correct dimensions and we can tether the parent */} +
+ {React.cloneElement(menu, { style: { position: 'static', width: selectWidth } })} + + ); + } +} diff --git a/app/scripts/modules/core/src/presentation/index.ts b/app/scripts/modules/core/src/presentation/index.ts index baf8e34bc9a..eed703ba580 100644 --- a/app/scripts/modules/core/src/presentation/index.ts +++ b/app/scripts/modules/core/src/presentation/index.ts @@ -8,6 +8,7 @@ export * from './ReactModal'; export * from './RenderOutputFile'; export * from './SpanDropdownTrigger'; export * from './TetheredSelect'; +export * from './TetheredCreatable'; export * from './Tooltip'; export * from './WatchValue'; export * from './collapsibleSection/CollapsibleSection';