Skip to content

Commit

Permalink
metal3: support edit for Bare Metal Hosts
Browse files Browse the repository at this point in the history
  • Loading branch information
suomiy committed Jan 17, 2020
1 parent bb5bf84 commit a271690
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,74 @@ import * as _ from 'lodash';
import { Formik } from 'formik';
import { history, resourcePathFromModel, FirehoseResult } from '@console/internal/components/utils';
import { nameValidationSchema } from '@console/dev-console/src/components/import/validation-schema';
import { getName } from '@console/shared';
import { createBareMetalHost } from '../../../k8s/requests/bare-metal-host';
import { getName } from '@console/shared/src';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { createBareMetalHost, updateBareMetalHost } from '../../../k8s/requests/bare-metal-host';
import { BareMetalHostModel } from '../../../models';
import { BareMetalHostKind } from '../../../types';
import {
buildBareMetalHostSecret,
buildBareMetalHostObject,
} from '../../../k8s/objects/bare-metal-host';
getHostBMCAddress,
getHostBootMACAddress,
getHostDescription,
isHostOnline,
} from '../../../selectors';
import { getSecretPassword, getSecretUsername } from '../../../selectors/secret';
import { getLoadedData } from '../../../utils';
import { usePrevious } from '../../../hooks';
import AddBareMetalHostForm from './AddBareMetalHostForm';
import { AddBareMetalHostFormValues } from './types';
import { MAC_REGEX, BMC_ADDRESS_REGEX } from './utils';
import { MAC_REGEX, BMC_ADDRESS_REGEX, AddBareMetalHostContext } from './utils';

const initialValues: AddBareMetalHostFormValues = {
name: '',
BMCAddress: '',
username: '',
password: '',
bootMACAddress: '',
online: true,
description: '',
};
const getInitialValues = (
host: BareMetalHostKind,
secret: K8sResourceKind,
): AddBareMetalHostFormValues => ({
name: getName(host) || '',
BMCAddress: getHostBMCAddress(host) || '',
username: getSecretUsername(secret) || '',
password: getSecretPassword(secret) || '',
bootMACAddress: getHostBootMACAddress(host) || '',
online: isHostOnline(host) || true,
description: getHostDescription(host) || '',
});

type AddBareMetalHostProps = {
namespace: string;
isEditing: boolean;
loaded?: boolean;
hosts?: FirehoseResult<BareMetalHostKind[]>;
host?: FirehoseResult<BareMetalHostKind>;
secret?: FirehoseResult<K8sResourceKind>;
};

const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({ namespace, hosts }) => {
const hostNames = _.flatMap(_.get(hosts, 'data', []), (host) => getName(host));
const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({
namespace,
isEditing,
hosts,
host: resultHost,
secret: resultSecret,
}) => {
const [reload, setReload] = React.useState<boolean>(false);
const hostNames = _.flatMap(getLoadedData(hosts, []), (host) => getName(host));
const initialHost = getLoadedData(resultHost);
const initialSecret = getLoadedData(resultSecret);
const prevInitialHost = usePrevious(initialHost);
const prevInitialSecret = usePrevious(initialSecret);

const initialValues = getInitialValues(initialHost, initialSecret);
const prevInitialValues = getInitialValues(prevInitialHost, prevInitialSecret);

React.useEffect(() => {
if (reload) {
setReload(false);
}
}, [reload, setReload]);

const showUpdated =
isEditing &&
prevInitialHost &&
prevInitialSecret &&
!_.isEqual(prevInitialValues, initialValues);

const addHostValidationSchema = Yup.object().shape({
name: Yup.mixed()
Expand All @@ -52,23 +91,16 @@ const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({ namespace, hosts })
.required('Required.'),
});

const handleSubmit = (
{ name, BMCAddress, username, password, bootMACAddress, online, description },
actions,
) => {
const secret = buildBareMetalHostSecret(name, namespace, username, password);
const bareMetalHost = buildBareMetalHostObject(
name,
namespace,
BMCAddress,
bootMACAddress,
online,
description,
);
createBareMetalHost(bareMetalHost, secret)
const handleSubmit = (values, actions) => {
const opts = { ...values, namespace };
const promise = isEditing
? updateBareMetalHost(initialHost, initialSecret, opts)
: createBareMetalHost(opts);

promise
.then(() => {
actions.setSubmitting(false);
history.push(resourcePathFromModel(BareMetalHostModel, name, namespace));
history.push(resourcePathFromModel(BareMetalHostModel, values.name, namespace));
})
.catch((error) => {
actions.setSubmitting(false);
Expand All @@ -77,13 +109,17 @@ const AddBareMetalHost: React.FC<AddBareMetalHostProps> = ({ namespace, hosts })
};

return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={history.goBack}
validationSchema={addHostValidationSchema}
component={AddBareMetalHostForm}
/>
<AddBareMetalHostContext.Provider value={{ isNameDisabled: isEditing, isEditing, showUpdated }}>
<Formik
initialValues={initialValues}
enableReinitialize={isEditing && (reload || !prevInitialHost || !prevInitialSecret)}
onSubmit={handleSubmit}
onReset={() => setReload(true)}
validationSchema={addHostValidationSchema}
component={AddBareMetalHostForm}
isNameDisabled={isEditing}
/>
</AddBareMetalHostContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react';
import * as _ from 'lodash';
import { history } from '@console/internal/components/utils';
import { FormikProps } from 'formik';
import { Form, TextInputTypes } from '@patternfly/react-core';
import { InputField, TextAreaField, SwitchField, FormFooter } from '@console/shared';
import { InputField, TextAreaField, SwitchField, FormFooter } from '@console/shared/src';
import { AddBareMetalHostFormValues } from './types';
import { AddBareMetalHostContext } from './utils';

type AddBareMetalHostFormProps = FormikProps<AddBareMetalHostFormValues>;

Expand All @@ -14,65 +16,76 @@ const AddBareMetalHostForm: React.FC<AddBareMetalHostFormProps> = ({
status,
isSubmitting,
dirty,
}) => (
<Form onSubmit={handleSubmit}>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-name-input"
name="name"
label="Name"
placeholder="openshift-worker"
helpText="Provide unique name for the new Bare Metal Host."
required
/>
<TextAreaField
data-test-id="add-baremetal-host-form-description-input"
name="description"
label="Description"
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-bmc-address-input"
name="BMCAddress"
label="BMC Address"
helpText="The URL for communicating with the BMC (Baseboard Management Controller) on the host, based on the provider being used."
required
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-username-input"
name="username"
label="BMC Username"
required
/>
<InputField
type={TextInputTypes.password}
data-test-id="add-baremetal-host-form-password-input"
name="password"
label="BMC Password"
required
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-boot-mac-address-input"
name="bootMACAddress"
label="Boot MAC Address"
helpText="The MAC address of the NIC connected to the network that will be used to provision the host."
required
/>
<SwitchField
name="online"
data-test-id="add-baremetal-host-form-online-switch"
label="Power host on after creation"
/>
<FormFooter
isSubmitting={isSubmitting}
handleCancel={handleReset}
submitLabel="Create"
errorMessage={status && status.submitError}
disableSubmit={isSubmitting || !dirty || !_.isEmpty(errors)}
/>
</Form>
);
}) => {
const { isNameDisabled, isEditing, showUpdated } = React.useContext<any>(AddBareMetalHostContext);

return (
<Form onSubmit={handleSubmit}>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-name-input"
name="name"
label="Name"
placeholder="openshift-worker"
helpText="Provide unique name for the new Bare Metal Host."
required
isDisabled={isNameDisabled}
/>
<TextAreaField
data-test-id="add-baremetal-host-form-description-input"
name="description"
label="Description"
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-bmc-address-input"
name="BMCAddress"
label="BMC Address"
helpText="The URL for communicating with the BMC (Baseboard Management Controller) on the host, based on the provider being used."
required
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-username-input"
name="username"
label="BMC Username"
required
/>
<InputField
type={TextInputTypes.password}
data-test-id="add-baremetal-host-form-password-input"
name="password"
label="BMC Password"
required
/>
<InputField
type={TextInputTypes.text}
data-test-id="add-baremetal-host-form-boot-mac-address-input"
name="bootMACAddress"
label="Boot MAC Address"
helpText="The MAC address of the NIC connected to the network that will be used to provision the host."
required
/>
{!isEditing && (
<SwitchField
name="online"
data-test-id="add-baremetal-host-form-online-switch"
label="Power host on after creation"
/>
)}
<FormFooter
isSubmitting={isSubmitting}
handleReset={showUpdated && handleReset}
handleCancel={history.goBack}
submitLabel={isEditing ? 'Save' : 'Create'}
errorMessage={status && status.submitError}
disableSubmit={isSubmitting || !dirty || !_.isEmpty(errors)}
infoTitle={'Bare Metal Host has been updated'}
infoMessage={'Click reload to see the recent changes'}
showAlert={showUpdated}
/>
</Form>
);
};

export default AddBareMetalHostForm;
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,47 @@ import { RouteComponentProps } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Firehose, FirehoseResource } from '@console/internal/components/utils';
import { referenceForModel } from '@console/internal/module/k8s';
import { SecretModel } from '@console/internal/models';
import { BareMetalHostModel } from '../../../models';
import { getSecretName } from '../../../k8s/objects/bare-metal-host';
import AddBareMetalHost from './AddBareMetalHost';

export type AddBareMetalHostPageProps = RouteComponentProps<{ ns?: string }>;
export type AddBareMetalHostPageProps = RouteComponentProps<{ ns?: string; name?: string }>;

const AddBareMetalHostPage: React.FunctionComponent<AddBareMetalHostPageProps> = ({ match }) => {
const namespace = match.params.ns;
const resources: FirehoseResource[] = [
{
const { name, ns: namespace } = match.params;
const resources: FirehoseResource[] = [];

const isEditing = !!name;
if (isEditing) {
resources.push(
{
kind: referenceForModel(BareMetalHostModel),
namespaced: true,
namespace,
name,
isList: false,
prop: 'host',
},
{
kind: SecretModel.kind,
namespaced: true,
namespace,
name: getSecretName(name),
isList: false,
prop: 'secret',
},
);
} else {
resources.push({
kind: referenceForModel(BareMetalHostModel),
namespaced: true,
namespace,
isList: true,
prop: 'hosts',
},
];
const title = 'Add Bare Metal Host';
});
}
const title = `${isEditing ? 'Edit' : 'Add'} Bare Metal Host`;
return (
<>
<Helmet>
Expand All @@ -31,11 +55,13 @@ const AddBareMetalHostPage: React.FunctionComponent<AddBareMetalHostPageProps> =
<h1 className="co-m-pane__heading co-m-pane__heading--baseline">
<div className="co-m-pane__name">{title}</div>
</h1>
<p className="co-m-pane__explanation">
Expand the hardware inventory by registering new Bare Metal Host.
</p>
{!isEditing && (
<p className="co-m-pane__explanation">
Expand the hardware inventory by registering new Bare Metal Host.
</p>
)}
<Firehose resources={resources}>
<AddBareMetalHost namespace={namespace} />
<AddBareMetalHost namespace={namespace} isEditing={isEditing} />
</Firehose>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as React from 'react';

const HEXCH_REGEX = '[0-9A-Fa-f]';
export const MAC_REGEX_COLON_DELIMITER = new RegExp(
`^((${HEXCH_REGEX}{2}[:]){19}${HEXCH_REGEX}{2})$|` + // 01:23:45:67:89:ab:cd:ef:00:00:01:23:45:67:89:ab:cd:ef:00:00
Expand Down Expand Up @@ -35,3 +37,5 @@ export const URL_REGEX = new RegExp(
export const BMC_ADDRESS_REGEX = new RegExp(
/^((ipmi|idrac|idrac\+http|idrac-virtualmedia|irmc|redfish|redfish\+http|redfish-virtualmedia|ilo5-virtualmedia|https?|ftp):(\/\/([a-z0-9\-._~%!$&'()*+,;=]+@)?([a-z0-9\-._~%]+|\[[a-f0-9:.]+\]|\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\])(:[0-9]+)?(\/[a-z0-9\-._~%!$&'()*+,;=:@]+)*\/?|(\/?[a-z0-9\-._~%!$&'()*+,;=:@]+(\/[a-z0-9\-._~%!$&'()*+,;=:@]+)*\/?)?)|([a-z0-9\-._~%!$&'()*+,;=@]+(\/[a-z0-9\-._~%!$&'()*+,;=:@]+)*\/?|(\/[a-z0-9\-._~%!$&'()*+,;=:@]+)+\/?))(\?[a-z0-9\-._~%!$&'()*+,;=:@/?]*)?(#[a-z0-9\-._~%!$&'()*+,;=:@/?]*)?$/i,
);

export const AddBareMetalHostContext = React.createContext({});

0 comments on commit a271690

Please sign in to comment.