Skip to content

Commit

Permalink
Bring back instantiate-template.tsx
Browse files Browse the repository at this point in the history
  • Loading branch information
maudem committed Aug 26, 2021
1 parent 1c00bb9 commit 1aa2a23
Showing 1 changed file with 387 additions and 0 deletions.
387 changes: 387 additions & 0 deletions frontend/public/components/instantiate-template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
import * as React from 'react';
import { connect } from 'react-redux';
import * as _ from 'lodash-es';
import { Helmet } from 'react-helmet';
import * as classNames from 'classnames';
import { ActionGroup, Button } from '@patternfly/react-core';

import { ANNOTATIONS, withActivePerspective } from '@console/shared';
import { isPerspective, Perspective, withExtensions } from '@console/plugin-sdk';
import * as catalogImg from '../imgs/logos/catalog-icon.svg';
import {
getImageForIconClass,
getTemplateIcon,
normalizeIconClass,
} from './catalog/catalog-item-icon';
import {
ButtonBar,
ExternalLink,
Firehose,
history,
LoadingBox,
LoadError,
NsDropdown,
} from './utils';
import { SecretModel, TemplateInstanceModel } from '../models';
import {
k8sCreate,
K8sResourceKind,
TemplateKind,
TemplateInstanceKind,
TemplateParameter,
} from '../module/k8s';
import { RootState } from '../redux';

const TemplateResourceDetails: React.FC<TemplateResourceDetailsProps> = ({ template }) => {
const resources = _.uniq(_.compact(_.map(template.objects, 'kind'))).sort();
if (_.isEmpty(resources)) {
return null;
}

return (
<>
<hr />
<p>The following resources will be created:</p>
<ul>
{resources.map((kind: string) => (
<li key={kind}>{kind}</li>
))}
</ul>
</>
);
};
TemplateResourceDetails.displayName = 'TemplateResourceDetails';

const TemplateInfo: React.FC<TemplateInfoProps> = ({ template }) => {
const annotations = template.metadata.annotations || {};
const { description } = annotations;
const displayName = annotations[ANNOTATIONS.displayName] || template.metadata.name;
const iconClass = getTemplateIcon(template);
const imgURL = iconClass ? getImageForIconClass(iconClass) : catalogImg;
const tags = (annotations.tags || '').split(/\s*,\s*/);
const documentationURL = annotations[ANNOTATIONS.documentationURL];
const supportURL = annotations[ANNOTATIONS.supportURL];

return (
<div className="co-catalog-item-info">
<div className="co-catalog-item-details">
<span className="co-catalog-item-icon">
{imgURL ? (
<img
className="co-catalog-item-icon__img co-catalog-item-icon__img--large"
src={imgURL}
alt={displayName}
aria-hidden
/>
) : (
<span
className={classNames(
'co-catalog-item-icon__icon co-catalog-item-icon__icon--large',
normalizeIconClass(iconClass),
)}
aria-hidden
/>
)}
</span>
<div>
<h2 className="co-section-heading co-catalog-item-details__name">{displayName}</h2>
{!_.isEmpty(tags) && (
<p className="co-catalog-item-details__tags">
{_.map(tags, (tag, i) => (
<span className="co-catalog-item-details__tag" key={i}>
{tag}
</span>
))}
</p>
)}
{(documentationURL || supportURL) && (
<ul className="list-inline">
{documentationURL && (
<li className="co-break-word">
<ExternalLink href={documentationURL} text="View documentation" />
</li>
)}
{supportURL && (
<li className="co-break-word">
<ExternalLink href={supportURL} text="Get support" />
</li>
)}
</ul>
)}
</div>
</div>
{description && <p className="co-catalog-item-details__description">{description}</p>}
<TemplateResourceDetails template={template} />
</div>
);
};
TemplateInfo.displayName = 'TemplateInfo';

const stateToProps = (state: RootState) => ({
models: state.k8s.getIn(['RESOURCES', 'models']),
});

class TemplateForm_ extends React.Component<TemplateFormProps, TemplateFormState> {
constructor(props: TemplateFormProps) {
super(props);

const { preselectedNamespace: namespace = '' } = this.props;
const parameters = this.getParameterValues(props);
this.state = {
namespace,
parameters,
inProgress: false,
error: '',
};
}

componentDidUpdate(prevProps: TemplateFormProps) {
if (this.props.obj !== prevProps.obj) {
const parameters = this.getParameterValues(this.props);
this.setState({ parameters });
}
}

getParameterValues = (props: TemplateFormProps) => {
const templateParameters: TemplateParameter[] = props.obj.data.parameters || [];
return templateParameters.reduce((acc, { name, value }: TemplateParameter) => {
acc[name] = value;
return acc;
}, {});
};

onNamespaceChange = (namespace: string) => {
this.setState({ namespace });
};

onParameterChanged: React.ReactEventHandler<HTMLInputElement> = (event) => {
const { name, value } = event.currentTarget;
this.setState(({ parameters }) => ({
parameters: {
...parameters,
[name]: value,
},
}));
};

createTemplateSecret(): Promise<K8sResourceKind> {
const { obj } = this.props;
const { namespace, parameters } = this.state;
const secret = {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
generateName: `${obj.data.metadata.name}-parameters-`,
namespace,
},
// Remove empty values.
stringData: parameters,
};
return k8sCreate(SecretModel, secret);
}

createTemplateInstance(secret: K8sResourceKind): Promise<K8sResourceKind> {
const { obj } = this.props;
const { namespace } = this.state;
const instance: TemplateInstanceKind = {
apiVersion: 'template.openshift.io/v1',
kind: 'TemplateInstance',
metadata: {
generateName: `${obj.data.metadata.name}-`,
namespace,
},
spec: {
template: obj.data as TemplateKind,
secret: {
name: secret.metadata.name,
},
},
};
return k8sCreate(TemplateInstanceModel, instance);
}

save = (event: React.FormEvent<EventTarget>) => {
event.preventDefault();
const { namespace } = this.state;
if (!namespace) {
this.setState({ error: 'Please complete all fields.' });
return;
}
const { activePerspective, perspectiveExtensions } = this.props;

this.setState({ error: '', inProgress: true });
this.createTemplateSecret()
.then((secret: K8sResourceKind) => {
return this.createTemplateInstance(secret).then(() => {
this.setState({ inProgress: false });
const activeExtension = perspectiveExtensions.find(
(p) => p.properties.id === activePerspective,
);
history.push(activeExtension.properties.getImportRedirectURL(namespace));
});
})
.catch((err) => this.setState({ inProgress: false, error: err.message }));
};

render() {
const { obj } = this.props;
if (obj.loadError) {
return (
<LoadError
message={obj.loadError.message}
label="Template"
className="loading-box loading-box__errored"
/>
);
}

if (!obj.loaded) {
return <LoadingBox />;
}

const template: TemplateKind = obj.data;
const parameters = template.parameters || [];

return (
<div className="row">
<div className="col-md-7 col-md-push-5 co-catalog-item-info">
<TemplateInfo template={template} />
</div>
<div className="col-md-5 col-md-pull-7">
<form className="co-instantiate-template-form" onSubmit={this.save}>
<div className="form-group">
<label className="control-label co-required" htmlFor="namespace">
Namespace
</label>
<NsDropdown
selectedKey={this.state.namespace}
onChange={this.onNamespaceChange}
id="namespace"
/>
</div>
{parameters.map(
({
name,
displayName,
description,
required: requiredParam,
generate,
}: TemplateParameter) => {
const value = this.state.parameters[name] || '';
const helpID = description ? `${name}-help` : '';
const placeholder = generate ? '(generated if empty)' : '';
// Only set required for parameters not generated.
const requiredInput = requiredParam && !generate;
return (
<div className="form-group" key={name}>
<label
className={classNames('control-label', { 'co-required': requiredInput })}
htmlFor={name}
>
{displayName || name}
</label>
<input
type="text"
className="pf-c-form-control"
id={name}
name={name}
value={value}
onChange={this.onParameterChanged}
required={requiredInput}
placeholder={placeholder}
aria-describedby={helpID}
/>
{description && (
<div className="help-block" id={helpID}>
{description}
</div>
)}
</div>
);
},
)}
<ButtonBar
className="co-instantiate-template-form__button-bar"
errorMessage={this.state.error}
inProgress={this.state.inProgress}
>
<ActionGroup className="pf-c-form">
<Button type="submit" variant="primary">
Create
</Button>
<Button type="button" variant="secondary" onClick={history.goBack}>
Cancel
</Button>
</ActionGroup>
</ButtonBar>
</form>
</div>
</div>
);
}
}

const TemplateForm = connect(stateToProps)(
withExtensions<ExtensionsProps>({ perspectiveExtensions: isPerspective })(
withActivePerspective<TemplateFormProps>(TemplateForm_),
),
);

export const InstantiateTemplatePage: React.FC<{}> = (props) => {
const title = 'Instantiate Template';
const searchParams = new URLSearchParams(location.search);
const templateName = searchParams.get('template');
const templateNamespace = searchParams.get('template-ns');
const preselectedNamespace = searchParams.get('preselected-ns');
const resources = [
{
kind: 'Template',
name: templateName,
namespace: templateNamespace,
isList: false,
prop: 'obj',
},
];

return (
<>
<Helmet>
<title>{title}</title>
</Helmet>
<div className="co-m-pane__body">
<h1 className="co-m-pane__heading">{title}</h1>
<Firehose resources={resources}>
<TemplateForm preselectedNamespace={preselectedNamespace} {...(props as any)} />
</Firehose>
</div>
</>
);
};

type TemplateResourceDetailsProps = {
template: TemplateKind;
};

type TemplateInfoProps = {
template: TemplateKind;
};

type ExtensionsProps = {
perspectiveExtensions: Perspective[];
};

type TemplateFormProps = ExtensionsProps & {
obj: any;
preselectedNamespace: string;
models: any;
activePerspective: string;
};

type TemplateFormState = {
namespace: string;
parameters: {
[name: string]: string;
};
inProgress: boolean;
error: string;
};

0 comments on commit 1aa2a23

Please sign in to comment.