Skip to content
This repository was archived by the owner on Apr 28, 2020. It is now read-only.

Add a component for creating baremetal hosts #292

Merged
merged 1 commit into from
Apr 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sass/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
@import './components/ClusterOverview/health';
@import './components/ClusterOverview/compliance';
@import './components/ClusterOverview/utilization';
@import './components/CreateBaremetalHostDialog/create-baremetal-host-dialog';

/*
TODO: these styles should be backported to the corresponding PF-React package
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.kubevirt-create-baremetal-host-dialog--paragraph {
margin-top: 20px;
}

.kubevirt-create-baremetal-host-dialog .modal-content {
max-height: 500px;
}
123 changes: 123 additions & 0 deletions src/components/CreateBaremetalHostDialog/CreateBaremetalHostDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, Col, Button, Alert } from 'patternfly-react';

import { FormFactory } from '../Form/FormFactory';
import { createBaremetalHost } from '../../k8s/request';
import { TinyInlineLoading } from '../Loading/Loading';
import { CREATE_HOST_FORM_TEXT } from './strings';

const formFields = {
name: {
id: 'name',
title: 'Name',
required: false,
},
controller: {
id: 'controller',
title: 'Management Controller Address',
required: true,
},
username: {
id: 'username',
title: 'Username',
required: true,
},
password: {
id: 'password',
title: 'Password',
type: 'password',
required: true,
},
};

export class CreateBaremetalHostDialog extends React.Component {
state = {
form: {
value: {},
valid: false,
},
isSubmitting: false,
errorMessage: null,
};

onFormChange = (newValue, target, formValid) => {
this.setState(state => {
const form = { ...state.form };
form.value[target] = {
...newValue,
};
form.valid = formValid;
return { form, errorMessage: null };
});
};

onSubmit = async () => {
this.setState(state => ({
...state,
isSubmitting: true,
}));

return createBaremetalHost(this.props.k8sCreate, this.state.form.value, this.props.selectedNamespace)
.then(result => {
this.setState(state => ({
...state,
isSubmitting: false,
}));
this.props.onClose();
return result;
})
.catch(({ message }) => {
this.setState(state => ({
...state,
isSubmitting: false,
errorMessage: message,
}));
});
};

render() {
return (
<Modal show bsSize="large" onHide={this.props.onClose} className="kubevirt-create-baremetal-host-dialog">
<Modal.Header closeButton>
<Modal.Title>Add Host</Modal.Title>
</Modal.Header>
<Modal.Body>
<Col sm={12}>
<p className="kubevirt-create-baremetal-host-dialog--paragraph">{CREATE_HOST_FORM_TEXT}</p>
<FormFactory
fields={formFields}
fieldsValues={this.state.form.value}
onFormChange={this.onFormChange}
horizontal={false}
/>
</Col>
</Modal.Body>
<Modal.Footer>
{this.state.errorMessage && <Alert type="error">{this.state.errorMessage}</Alert>}
{this.state.isSubmitting && <TinyInlineLoading />}
<Button bsStyle="default" onClick={this.props.onClose}>
Cancel
</Button>
<Button
bsStyle="primary"
disabled={!this.state.form.valid || this.state.isSubmitting}
onClick={this.onSubmit}
>
Add Host
</Button>
</Modal.Footer>
</Modal>
);
}
}

CreateBaremetalHostDialog.defaultProps = {
selectedNamespace: null,
};

CreateBaremetalHostDialog.propTypes = {
k8sCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
selectedNamespace: PropTypes.object,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CreateBaremetalHostDialog } from '../CreateBaremetalHostDialog';

export default {
component: CreateBaremetalHostDialog,
props: {
k8sCreate: () => {},
onClose: () => {},
selectedNamespace: {},
},
};
1 change: 1 addition & 0 deletions src/components/CreateBaremetalHostDialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CreateBaremetalHostDialog';
1 change: 1 addition & 0 deletions src/components/CreateBaremetalHostDialog/strings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CREATE_HOST_FORM_TEXT = `Specify the host's name, management controller IP address, and credentials to add it to the cluster.`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';

import CreateBaremetalHostDialogFixture from '../fixtures/CreateBaremetalHostDialog.fixture';
import { CreateBaremetalHostDialog } from '../CreateBaremetalHostDialog';

const testCreateBaremetalHostDialog = () => <CreateBaremetalHostDialog {...CreateBaremetalHostDialogFixture.props} />;

describe('<CreateBaremetalHostDialog />', () => {
it('renders correctly', () => {
const component = shallow(testCreateBaremetalHostDialog());
expect(component).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<CreateBaremetalHostDialog /> renders correctly 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
bsSize="large"
className="kubevirt-create-baremetal-host-dialog"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h4"
>
Add Host
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<Col
bsClass="col"
componentClass="div"
sm={12}
>
<p
className="kubevirt-create-baremetal-host-dialog--paragraph"
>
Specify the host's name, management controller IP address, and credentials to add it to the cluster.
</p>
<FormFactory
controlSize={5}
fields={
Object {
"controller": Object {
"id": "controller",
"required": true,
"title": "Management Controller Address",
},
"name": Object {
"id": "name",
"required": false,
"title": "Name",
},
"password": Object {
"id": "password",
"required": true,
"title": "Password",
"type": "password",
},
"username": Object {
"id": "username",
"required": true,
"title": "Username",
},
}
}
fieldsValues={Object {}}
horizontal={false}
labelSize={3}
onFormChange={[Function]}
showLabels={true}
textPosition="text-right"
/>
</Col>
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<Button
active={false}
block={false}
bsClass="btn"
bsStyle="default"
disabled={false}
onClick={[Function]}
>
Cancel
</Button>
<Button
active={false}
block={false}
bsClass="btn"
bsStyle="primary"
disabled={true}
onClick={[Function]}
>
Add Host
</Button>
</ModalFooter>
</Modal>
`;
2 changes: 2 additions & 0 deletions src/components/Loading/Loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import PropTypes from 'prop-types';

import { prefixedId } from '../../utils';

export const TinyInlineLoading = () => <span className="spinner spinner-xs spinner-inline" />;

export const InlineLoading = ({ id, size }) => (
<div id={id} key={prefixedId(id, 'progress') || 'progress'}>
<div className={`spinner spinner-${size} blank-slate-pf-icon`} />
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './components/Dashboard';
export * from './components/Details';
export * from './components/Dialog';
export * from './components/Form';
export * from './components/CreateBaremetalHostDialog';
// Loading component group not exported
// Table component group not exported
export * from './components/TemplateSource';
Expand Down
3 changes: 3 additions & 0 deletions src/k8s/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getDataVolumeStorageClassName,
getPvcStorageClassName,
} from '../selectors';

import { VirtualMachineModel, ProcessedTemplatesModel, TemplateModel, DataVolumeModel, SecretModel } from '../models';

import {
Expand Down Expand Up @@ -104,6 +105,8 @@ import {

import { getImportProviderSecretObject } from '../components/Wizard/CreateVmWizard/providers/vmwareProviderPod';

export * from './requests/hosts';

const FALLBACK_DISK = {
disk: {
bus: 'virtio',
Expand Down
50 changes: 50 additions & 0 deletions src/k8s/requests/hosts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SecretModel, BaremetalHostModel } from '../../../models';
import { getName } from '../../../selectors';

const getSecretName = name => `${name}-secret`;

const getSecret = (name, namespace, username, password) => ({
apiVersion: SecretModel.apiVersion,
kind: SecretModel.kind,
metadata: {
namespace,
name,
},
data: {
username: btoa(username),
password: btoa(password),
},
type: 'Opaque',
});

const getBaremetalHostObject = (name, namespace, controller, secretName) => ({
apiVersion: `${BaremetalHostModel.apiGroup}/${BaremetalHostModel.apiVersion}`,
kind: BaremetalHostModel.kind,
metadata: {
name,
namespace,
},
spec: {
bmc: {
address: controller,
credentialsName: secretName,
},
},
});

export const createBaremetalHost = async (k8sCreate, formData, selectedNamespace) => {
const { name, username, password, controller } = formData;

const namespace = getName(selectedNamespace);
const secretName = getSecretName(name.value);

const secret = getSecret(secretName, namespace, username.value, password.value);
const bmo = getBaremetalHostObject(name.value, namespace, controller.value, secretName);

const results = [];

results.push(await k8sCreate(SecretModel, secret));
results.push(await k8sCreate(BaremetalHostModel, bmo));

return results;
};