Skip to content

Commit

Permalink
fix(artifacts/helm): support regex/spel in version (#7033)
Browse files Browse the repository at this point in the history
* fix(artifacts/helm): support regex/spel in version

the version field in an artifact supports regex but the previous UI
forced a hardcoded version. this made triggers really confusing because
users are forced to set a version on the artifact when they really
wanted something like `.*` to indicate that this chart should be matched
for all versions.

* refactor(artifact/helm): address PR feedback
  • Loading branch information
ethanfrogers committed May 23, 2019
1 parent 59259a9 commit 4975233
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,74 +1,104 @@
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<Option<string>>;
versionsLoading: boolean;
namesLoading: boolean;
}

class HelmEditor extends React.Component<IArtifactEditorProps, IHelmArtifactEditorState> {
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: [],
});
});
}
}

public render() {
const { artifact } = this.props;
const nameOptions = this.state.names.map(name => ({ value: name, label: name }));
const versionOptions = this.state.versions.map(version => ({ value: version, label: version }));

return (
<>
<StageConfigField label="Name">
<TetheredSelect
className={'col-md-3'}
options={nameOptions}
value={artifact.name || ''}
onChange={(e: Option) => {
this.onChange(e, 'name');
this.onNameChange(e.value.toString());
}}
clearable={false}
/>
{!this.state.namesLoading && (
<TetheredSelect
options={nameOptions}
value={artifact.name || ''}
onChange={(e: Option) => {
this.onChange(e, 'name');
this.getChartVersionOptions(e.value.toString());
}}
clearable={false}
/>
)}
{this.state.namesLoading && <Spinner />}
</StageConfigField>
<StageConfigField label="Version">
<TetheredSelect
className={'col-md-3'}
options={versionOptions}
value={artifact.version || ''}
onChange={(e: Option) => {
this.onChange(e, 'version');
}}
clearable={false}
/>
{!this.state.versionsLoading && (
<TetheredCreatable
options={this.state.versions}
value={artifact.version || ''}
onChange={(e: Option) => {
this.onChange(e, 'version');
}}
clearable={false}
/>
)}
{this.state.versionsLoading && <Spinner />}
</StageConfigField>
</>
);
Expand All @@ -80,11 +110,21 @@ class HelmEditor extends React.Component<IArtifactEditorProps, IHelmArtifactEdit
this.props.onChange(clone);
};

private onNameChange = (chartName: string) => {
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 = {
Expand Down
44 changes: 44 additions & 0 deletions app/scripts/modules/core/src/presentation/TetheredCreatable.tsx
Original file line number Diff line number Diff line change
@@ -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...
<TetherComponent
classes={{ element: 'layer-high' }}
attachment="top left"
targetAttachment="top left"
constraints={[
{
to: 'window',
attachment: 'together',
pin: ['top'],
},
]}
>
{/* Apply position:static to our menu so that its parent will get the correct dimensions and we can tether the parent */}
<div />
{React.cloneElement(menu, { style: { position: 'static', width: selectWidth } })}
</TetherComponent>
);
}
}
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/presentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 4975233

Please sign in to comment.