Skip to content

Commit

Permalink
Add support for dynamic forms in helm install and upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitkrai03 committed Jun 23, 2020
1 parent 91ba110 commit 6c21ed9
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';
import * as _ from 'lodash';
import * as Ajv from 'ajv';
import { JSONSchema6 } from 'json-schema';
import { safeDump, safeLoad } from 'js-yaml';
import { Formik } from 'formik';
import { Helmet } from 'react-helmet';
Expand All @@ -21,19 +23,24 @@ import { getHelmActionValidationSchema } from './helm-validation-utils';
import { getHelmActionConfig, getChartValuesYAML } from './helm-utils';
import NamespacedPage, { NamespacedPageVariants } from '../NamespacedPage';
import HelmInstallUpgradeForm from './form/HelmInstallUpgradeForm';
import HelmChartMetaDescription from './form/HelmChartMetaDescription';
import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle';

export type HelmInstallUpgradePageProps = RouteComponentProps<{
ns?: string;
releaseName?: string;
}>;

export type HelmInstallUpgradeFormData = {
helmReleaseName: string;
helmChartURL?: string;
releaseName: string;
chartURL?: string;
chartName: string;
chartValuesYAML: string;
chartVersion: string;
appVersion: string;
yamlData: string;
formData: any;
formSchema: JSONSchema6;
editorType: EditorType;
};

const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProps> = ({
Expand All @@ -42,25 +49,35 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp
}) => {
const searchParams = new URLSearchParams(location.search);

const chartURL = decodeURIComponent(searchParams.get('chartURL'));
const namespace = match.params.ns || searchParams.get('preselected-ns');
const releaseName = match.params.releaseName || '';
const initialChartURL = decodeURIComponent(searchParams.get('chartURL'));
const initialReleaseName = match.params.releaseName || '';
const helmChartName = searchParams.get('chartName');
const helmActionOrigin = searchParams.get('actionOrigin') as HelmActionOrigins;

const [chartDataLoaded, setChartDataLoaded] = React.useState<boolean>(false);
const [chartData, setChartData] = React.useState<HelmChart>(null);
const [chartName, setChartName] = React.useState<string>('');
const [chartHasValues, setChartHasValues] = React.useState<boolean>(false);
const [YAMLData, setYAMLData] = React.useState<string>('');
const [activeChartVersion, setActiveChartVersion] = React.useState<string>('');
const [chartVersion, setChartVersion] = React.useState<string>('');
const [appVersion, setAppVersion] = React.useState<string>('');
const [chartHasValues, setChartHasValues] = React.useState<boolean>(false);

const [initialYamlData, setInitialYamlData] = React.useState<string>('');
const [initialFormData, setInitialFormData] = React.useState<object>();
const [initialFormSchema, setInitialFormSchema] = React.useState<JSONSchema6>();

const helmAction: HelmActionType =
chartURL !== 'null' ? HelmActionType.Install : HelmActionType.Upgrade;
initialChartURL !== 'null' ? HelmActionType.Install : HelmActionType.Upgrade;

const config = React.useMemo<HelmActionConfigType>(
() => getHelmActionConfig(helmAction, releaseName, namespace, helmActionOrigin, chartURL),
[chartURL, helmAction, helmActionOrigin, namespace, releaseName],
() =>
getHelmActionConfig(
helmAction,
initialReleaseName,
namespace,
helmActionOrigin,
initialChartURL,
),
[helmAction, helmActionOrigin, initialChartURL, initialReleaseName, namespace],
);

React.useEffect(() => {
Expand All @@ -75,13 +92,17 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp
const chart: HelmChart = res?.chart || res;
const chartValues = getChartValuesYAML(chart);
const releaseValues = !_.isEmpty(res?.config) ? safeDump(res?.config) : '';
const values = releaseValues || chartValues;
setYAMLData(values);
const valuesYAML = releaseValues || chartValues;
const valuesJSON = (res?.config || chart?.values) ?? {};
const valuesSchema = chart?.schema && JSON.parse(atob(chart?.schema));
setInitialYamlData(valuesYAML);
setInitialFormData(valuesJSON);
setInitialFormSchema(valuesSchema);
setChartName(chart.metadata.name);
setActiveChartVersion(chart.metadata.version);
setChartVersion(chart.metadata.version);
setAppVersion(chart.metadata.appVersion);
setChartHasValues(!!values);
setChartDataLoaded(true);
setChartHasValues(!!valuesYAML);
setChartData(chart);
};

fetchHelmRelease();
Expand All @@ -92,21 +113,42 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp
}, [config.helmReleaseApi, helmAction]);

const initialValues: HelmInstallUpgradeFormData = {
helmReleaseName: releaseName || helmChartName || '',
helmChartURL: chartURL,
releaseName: initialReleaseName || helmChartName || '',
chartURL: initialChartURL,
chartName,
chartValuesYAML: YAMLData,
appVersion,
chartVersion: activeChartVersion,
chartVersion,
yamlData: initialYamlData,
formData: initialFormData,
formSchema: initialFormSchema,
editorType: initialFormSchema ? EditorType.Form : EditorType.YAML,
};

const handleSubmit = (values, actions) => {
actions.setStatus({ isSubmitting: true });
const { helmReleaseName, helmChartURL, chartValuesYAML }: HelmInstallUpgradeFormData = values;
const {
releaseName,
chartURL,
yamlData,
formData,
formSchema,
editorType,
}: HelmInstallUpgradeFormData = values;
let valuesObj;
if (chartValuesYAML) {

if (editorType === EditorType.Form) {
const ajv = new Ajv();
const validSchema = ajv.validateSchema(formSchema);
const validFormData = validSchema && ajv.validate(formSchema, formData);
if (validFormData) {
valuesObj = formData;
} else {
actions.setStatus({ submitError: `Errors in the Form - ${ajv.errorsText()}` });
return;
}
} else if (yamlData) {
try {
valuesObj = safeLoad(chartValuesYAML);
valuesObj = safeLoad(yamlData);
} catch (err) {
actions.setStatus({ submitError: `Invalid YAML - ${err}` });
return;
Expand All @@ -115,11 +157,8 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp

const payload = {
namespace,
name: helmReleaseName,
...(helmChartURL !== 'null' || undefined
? // eslint-disable-next-line @typescript-eslint/camelcase
{ chart_url: helmChartURL }
: {}),
name: releaseName,
...(chartURL !== 'null' || undefined ? { chart_url: chartURL } : {}), // eslint-disable-line @typescript-eslint/camelcase
...(valuesObj ? { values: valuesObj } : {}),
};

Expand Down Expand Up @@ -156,10 +195,12 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp
});
};

if (!chartDataLoaded) {
if (!chartData) {
return <LoadingBox />;
}

const chartMetaDescription = <HelmChartMetaDescription chart={chartData} />;

return (
<NamespacedPage variant={NamespacedPageVariants.light} disabled hideApplications>
<Helmet>
Expand All @@ -178,6 +219,7 @@ const HelmInstallUpgradePage: React.FunctionComponent<HelmInstallUpgradePageProp
{...formikProps}
chartHasValues={chartHasValues}
helmAction={helmAction}
chartMetaDescription={chartMetaDescription}
/>
)}
</Formik>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import * as _ from 'lodash';
import { HelmChart } from '../helm-types';
import { getImageForIconClass } from '@console/internal/components/catalog/catalog-item-icon';

type HelmChartMetaDescriptionProps = {
chart: HelmChart;
};

const HelmChartMetaDescription: React.FC<HelmChartMetaDescriptionProps> = ({ chart }) => {
const chartVersion = chart?.metadata?.version;
const displayName = _.startCase(chart?.metadata?.name);
const imgSrc = chart?.metadata?.icon || getImageForIconClass('icon-helm');
const provider = chart?.metadata?.maintainers?.[0]?.name;
return (
<div style={{ marginBottom: '30px' }}>
<div className="co-clusterserviceversion-logo">
<div className="co-clusterserviceversion-logo__icon">
<img
className="co-catalog-item-icon__img co-catalog-item-icon__img--large"
src={imgSrc}
alt=""
/>
</div>
<div className="co-clusterserviceversion-logo__name">
<h1 className="co-clusterserviceversion-logo__name__clusterserviceversion">
{displayName}
</h1>
{provider && (
<span className="co-clusterserviceversion-logo__name__provider text-muted">
{`${chartVersion || ''} provided by ${provider}`}
</span>
)}
</div>
</div>
{chart?.metadata?.description}
</div>
);
};

export default HelmChartMetaDescription;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DropdownField } from '@console/shared';
import { confirmModal } from '@console/internal/components/modals/confirm-modal';
import { k8sVersion } from '@console/internal/module/status';
import { getK8sGitVersion } from '@console/internal/module/k8s';
import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle';
import { HelmChartMetaData, HelmChart, HelmActionType, HelmChartEntries } from '../helm-types';
import { getChartURL, getChartVersions, getChartValuesYAML } from '../helm-utils';

Expand All @@ -25,13 +26,14 @@ const HelmChartVersionDropdown: React.FunctionComponent<HelmChartVersionDropdown
}) => {
const {
setFieldValue,
values: { chartValuesYAML, appVersion },
values: { yamlData, formData, appVersion },
setFieldTouched,
} = useFormikContext<FormikValues>();
const [helmChartVersions, setHelmChartVersions] = React.useState({});
const [helmChartEntries, setHelmChartEntries] = React.useState<HelmChartMetaData[]>([]);
const [initialChartYAMLValues, setInitialChartYAMLValues] = React.useState('');
const [kubernetesVersion, setKubernetesVersion] = React.useState<string>();
const [initialYamlData, setInitialYamlData] = React.useState<string>('');
const [initialFormData, setInitialFormData] = React.useState<object>();

const warnOnChartVersionChange = (
onAccept: ModalCallback,
Expand Down Expand Up @@ -69,7 +71,8 @@ const HelmChartVersionDropdown: React.FunctionComponent<HelmChartVersionDropdown
}, []);

React.useEffect(() => {
setInitialChartYAMLValues(chartValuesYAML);
setInitialYamlData(yamlData);
setInitialFormData(formData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down Expand Up @@ -100,23 +103,31 @@ const HelmChartVersionDropdown: React.FunctionComponent<HelmChartVersionDropdown
const chartURL = getChartURL(helmChartEntries, value);

setFieldValue('chartVersion', value);
setFieldValue('helmChartURL', chartURL);
setFieldValue('chartURL', chartURL);

coFetchJSON(`/api/helm/chart?url=${chartURL}`)
.then((res: HelmChart) => {
const chartValues = getChartValuesYAML(res);
setFieldValue('chartValuesYAML', chartValues);
setInitialChartYAMLValues(chartValues);
const valuesYAML = getChartValuesYAML(res);
const valuesJSON = res?.values;
const valuesSchema = res?.schema && JSON.parse(atob(res?.schema));
const editorType = valuesSchema ? EditorType.Form : EditorType.YAML;
setFieldValue('editorType', editorType);
setFieldValue('formSchema', valuesSchema);

setFieldValue('yamlData', valuesYAML);
setFieldValue('formData', valuesJSON);
setInitialYamlData(valuesYAML);
setInitialFormData(valuesJSON);
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
console.error(err); // eslint-disable-line no-console
});
};

const handleChartVersionChange = (val: string) => {
if (val !== chartVersion) {
const isDirty = !_.isEqual(initialChartYAMLValues, chartValuesYAML);
const isDirty =
!_.isEqual(initialYamlData, yamlData) || !_.isEqual(initialFormData, formData);
if (isDirty) {
warnOnChartVersionChange(() => onChartVersionChange(val), chartVersion, val);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import * as React from 'react';
import * as _ from 'lodash';
import { FormikProps, FormikValues } from 'formik';
import { TextInputTypes, Grid, GridItem } from '@patternfly/react-core';
import { InputField, FormFooter, FlexForm, YAMLEditorField } from '@console/shared';
import {
InputField,
FormFooter,
FlexForm,
YAMLEditorField,
DynamicFormField,
SyncedEditorField,
} from '@console/shared';
import FormSection from '../../import/section/FormSection';
import { HelmActionType } from '../helm-types';
import HelmChartVersionDropdown from './HelmChartVersionDropdown';

export interface HelmInstallUpgradeFormProps {
chartHasValues: boolean;
helmAction: string;
chartMetaDescription: React.ReactNode;
}

const HelmInstallUpgradeForm: React.FC<FormikProps<FormikValues> & HelmInstallUpgradeFormProps> = ({
Expand All @@ -22,18 +30,23 @@ const HelmInstallUpgradeForm: React.FC<FormikProps<FormikValues> & HelmInstallUp
helmAction,
values,
dirty,
chartMetaDescription,
}) => {
const { chartName, chartVersion } = values;
const { chartName, chartVersion, formData, formSchema } = values;
const isSubmitDisabled =
(helmAction === HelmActionType.Upgrade && !dirty) || status?.isSubmitting || !_.isEmpty(errors);
const formEditor = formData && formSchema && (
<DynamicFormField name="formData" schema={formSchema} formDescription={chartMetaDescription} />
);
const yamlEditor = chartHasValues && <YAMLEditorField name="yamlData" onSave={handleSubmit} />;
return (
<FlexForm onSubmit={handleSubmit}>
<FormSection fullWidth>
<Grid gutter={'md'}>
<GridItem span={6}>
<InputField
type={TextInputTypes.text}
name="helmReleaseName"
name="releaseName"
label="Release Name"
helpText="A unique name for the Helm Chart release."
required
Expand All @@ -47,14 +60,19 @@ const HelmInstallUpgradeForm: React.FC<FormikProps<FormikValues> & HelmInstallUp
/>
</Grid>
</FormSection>
{chartHasValues && <YAMLEditorField name="chartValuesYAML" onSave={handleSubmit} />}
<SyncedEditorField
name="editorType"
formContext={{ name: 'formData', editor: formEditor, isDisabled: !formSchema }}
yamlContext={{ name: 'yamlData', editor: yamlEditor }}
/>
<FormFooter
handleReset={handleReset}
errorMessage={status && status.submitError}
isSubmitting={status?.isSubmitting || isSubmitting}
submitLabel={helmAction}
disableSubmit={isSubmitDisabled}
resetLabel="Cancel"
sticky
/>
</FlexForm>
);
Expand Down

0 comments on commit 6c21ed9

Please sign in to comment.