diff --git a/src/components/common/DynamicListForm.jsx b/src/components/common/DynamicListForm.jsx deleted file mode 100644 index 70976f794..000000000 --- a/src/components/common/DynamicListForm.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button } from "@patternfly/react-core/dist/esm/components/Button"; -import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState"; -import { FormFieldGroup, FormFieldGroupHeader } from "@patternfly/react-core/dist/esm/components/Form"; -import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText"; - -import './DynamicListForm.scss'; - -export class DynamicListForm extends React.Component { - constructor(props) { - super(props); - this.state = { - list: [], - }; - this.keyCounter = 0; - this.removeItem = this.removeItem.bind(this); - this.addItem = this.addItem.bind(this); - this.onItemChange = this.onItemChange.bind(this); - } - - removeItem(idx, field, value) { - this.setState(state => { - const items = state.list.concat(); - items.splice(idx, 1); - return { list: items }; - }, () => this.props.onChange(this.state.list.concat())); - } - - addItem() { - this.setState(state => { - return { list: [...state.list, Object.assign({ key: this.keyCounter++ }, this.props.default)] }; - }, () => this.props.onChange(this.state.list.concat())); - } - - onItemChange(idx, field, value) { - this.setState(state => { - const items = state.list.concat(); - items[idx][field] = value || null; - return { list: items }; - }, () => this.props.onChange(this.state.list.concat())); - } - - render () { - const { id, label, actionLabel, formclass, emptyStateString, helperText } = this.props; - const dialogValues = this.state; - return ( - {actionLabel}} - /> - } className={"dynamic-form-group " + formclass}> - { - dialogValues.list.length - ? <> - {dialogValues.list.map((item, idx) => { - return React.cloneElement(this.props.itemcomponent, { - idx, - item, - id: id + "-" + idx, - key: idx, - onChange: this.onItemChange, - removeitem: this.removeItem, - additem: this.addItem, - options: this.props.options, - itemCount: Object.keys(dialogValues.list).length, - }); - }) - } - {helperText && - - {helperText} - - } - - : - - {emptyStateString} - - - } - - ); - } -} -DynamicListForm.propTypes = { - emptyStateString: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - id: PropTypes.string.isRequired, - itemcomponent: PropTypes.object.isRequired, - formclass: PropTypes.string, - options: PropTypes.object, -}; diff --git a/src/components/common/DynamicListForm.scss b/src/components/common/DynamicListForm.scss deleted file mode 100644 index 1dfe40db8..000000000 --- a/src/components/common/DynamicListForm.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import "global-variables"; - -.dynamic-form-group { - .pf-v5-c-empty-state { - padding: 0; - } - - .pf-v5-c-form__label { - // Don't allow labels to wrap - white-space: nowrap; - } - - .remove-button-group { - // Move 'Remove' button the the end of the row - grid-column: -1; - // Move 'Remove' button to the bottom of the line so as to align with the other form fields - display: flex; - align-items: flex-end; - } - - // Set check to the same height as input widgets and vertically align - .pf-v5-c-form__group-control > .pf-v5-c-check { - // Set height to the same as inputs - // Font height is font size * line height (1rem * 1.5) - // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12 - // This all equals to 36px - height: calc(var(--pf-v5-global--FontSize--md) * var(--pf-v5-global--LineHeight--md) + 12px); - align-content: center; - } - - // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header - // However we want to save space and not add indent to the left so we need to override it - .pf-v5-c-form__field-group-body { - // Stretch content fully - --pf-v5-c-form__field-group-body--GridColumn: 1 / -1; - // Reduce padding at the top - --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs); - } -} diff --git a/src/components/create-vm-dialog/createVmDialog.jsx b/src/components/create-vm-dialog/createVmDialog.jsx index d1e9dfed1..c5bd1ae0e 100644 --- a/src/components/create-vm-dialog/createVmDialog.jsx +++ b/src/components/create-vm-dialog/createVmDialog.jsx @@ -24,6 +24,7 @@ import { Divider } from "@patternfly/react-core/dist/esm/components/Divider"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex"; import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form"; import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect"; +import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid"; import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup"; import { Modal } from "@patternfly/react-core/dist/esm/components/Modal"; import { Select as PFSelect, SelectGroup, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select"; @@ -33,7 +34,7 @@ import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip"; import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea"; import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner"; -import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { ExternalLinkAltIcon, TrashIcon } from '@patternfly/react-icons'; import { DialogsContext } from 'dialogs.jsx'; import cockpit from 'cockpit'; @@ -84,6 +85,7 @@ import { domainCreate } from '../../libvirtApi/domain.js'; import { storagePoolRefresh } from '../../libvirtApi/storagePool.js'; import { getAccessToken } from '../../libvirtApi/rhel-images.js'; import { PasswordFormFields, password_quality } from 'cockpit-components-password.jsx'; +import { DynamicListForm } from 'DynamicListForm.jsx'; import './createVmDialog.scss'; @@ -248,6 +250,8 @@ function validateParams(vmParams) { } if (vmParams.userPassword && !vmParams.userLogin) { validationFailed.userLogin = _("User login must not be empty when user password is set"); + } else if (vmParams.sshKeys.length > 0 && !vmParams.userLogin) { + validationFailed.userLogin = _("User login must not be empty when SSH keys are set"); } return validationFailed; @@ -748,6 +752,58 @@ const UsersConfigurationRow = ({ ); }; +// This method needs to be outside of component as re-render would create a new instance of debounce +const validateKey = debounce(500, (key, setKeyObject) => { + cockpit.script(`echo ${key} | ssh-keygen -l -f /dev/stdin`, {}) + .then(() => { + const parts = key.split(" "); + if (parts.length > 2) { + setKeyObject({ + type: parts[0], + data: parts[1], + comment: parts[2], // comment is optional in SSH-format + }); + } + }) + .catch(() => setKeyObject(undefined)); +}); + +const SshKeysRow = ({ + id, item, onChange, idx, removeitem, +}) => { + const [keyObject, setKeyObject] = useState(); + + return ( + + + {keyObject + ? + {keyObject.comment} + {keyObject.comment ? " - " + keyObject.type : keyObject.type} +
{keyObject.data}
+
+ :