finish core InventoriesLookup core functionality

This commit is contained in:
Keith Grant
2019-07-19 11:06:51 -07:00
parent c080346751
commit 5edc6deeae
10 changed files with 209 additions and 69 deletions

View File

@@ -97,7 +97,7 @@
width: 600px; width: 600px;
.pf-c-modal-box__body { .pf-c-modal-box__body {
overflow: visible; overflow: auto;
} }
.pf-c-modal-box__footer > .pf-c-button:not(:last-child) { .pf-c-modal-box__footer > .pf-c-button:not(:last-child) {

View File

@@ -107,6 +107,7 @@ class SelectResourceStep extends React.Component {
itemId={item.id} itemId={item.id}
key={item.id} key={item.id}
name={item[displayKey]} name={item[displayKey]}
label={item[displayKey]}
onSelect={() => onRowClick(item)} onSelect={() => onRowClick(item)}
/> />
)} )}

View File

@@ -7,42 +7,50 @@ import {
DataListCheck, DataListCheck,
DataListCell, DataListCell,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import DataListRadio from '@components/DataListRadio';
import VerticalSeparator from '../VerticalSeparator'; import VerticalSeparator from '../VerticalSeparator';
const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => ( const CheckboxListItem = ({ itemId, name, label, isSelected, onSelect, isRadio }) => {
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}> const CheckboxRadio = isRadio ? DataListRadio : DataListCheck;
<DataListItemRow> return (
<DataListCheck <DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
id={`selected-${itemId}`} <DataListItemRow>
checked={isSelected} <CheckboxRadio
onChange={onSelect} id={`selected-${itemId}`}
aria-labelledby={`check-action-item-${itemId}`} checked={isSelected}
value={itemId} onChange={onSelect}
/> aria-labelledby={`check-action-item-${itemId}`}
<DataListItemCells name={name}
dataListCells={[ value={itemId}
<DataListCell key="divider" className="pf-c-data-list__cell--divider"> />
<VerticalSeparator /> <DataListItemCells
</DataListCell>, dataListCells={[
<DataListCell key="name"> <DataListCell
<label key="divider"
id={`check-action-item-${itemId}`} className="pf-c-data-list__cell--divider"
htmlFor={`selected-${itemId}`}
className="check-action-item"
> >
<b>{name}</b> <VerticalSeparator />
</label> </DataListCell>,
</DataListCell>, <DataListCell key="name">
]} <label
/> id={`check-action-item-${itemId}`}
</DataListItemRow> htmlFor={`selected-${itemId}`}
</DataListItem> className="check-action-item"
); >
<b>{label}</b>
</label>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
);
};
CheckboxListItem.propTypes = { CheckboxListItem.propTypes = {
itemId: PropTypes.number.isRequired, itemId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired, isSelected: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
}; };

View File

@@ -9,6 +9,7 @@ describe('CheckboxListItem', () => {
<CheckboxListItem <CheckboxListItem
itemId={1} itemId={1}
name="Buzz" name="Buzz"
label="Buzz"
isSelected={false} isSelected={false}
onSelect={() => {}} onSelect={() => {}}
/> />

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import { string, bool, func } from 'prop-types';
import styled from 'styled-components';
function DataListRadio({
className = '',
onChange,
isValid = true,
isDisabled = false,
isChecked = null,
checked = null,
...props
}) {
return (
<div className={`pf-c-data-list__item-control ${className}`}>
<div className="pf-c-data-list__check">
<input
{...props}
type="radio"
onChange={event => onChange(event.currentTarget.checked, event)}
aria-invalid={!isValid}
disabled={isDisabled}
checked={isChecked || checked}
/>
</div>
</div>
);
}
DataListRadio.propTypes = {
className: string,
isValid: bool,
isDisabled: bool,
isChecked: bool,
checked: bool,
onChange: func,
'aria-labelledby': string,
};
DataListRadio.defaultProps = {
className: '',
isValid: true,
isDisabled: false,
isChecked: false,
checked: false,
onChange: () => {},
'aria-labelledby': '',
}
export default DataListRadio;

View File

@@ -0,0 +1 @@
export { default } from './DataListRadio';

View File

@@ -1,5 +1,13 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import {
string,
bool,
arrayOf,
func,
number,
oneOfType,
shape,
} from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { SearchIcon } from '@patternfly/react-icons'; import { SearchIcon } from '@patternfly/react-icons';
import { import {
@@ -20,19 +28,24 @@ import { ChipGroup, Chip } from '../Chip';
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs'; import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
const InputGroup = styled(PFInputGroup)` const InputGroup = styled(PFInputGroup)`
${props => props.multiple && (` ${props =>
props.multiple &&
`
--pf-c-form-control--Height: 90px; --pf-c-form-control--Height: 90px;
overflow-y: auto; overflow-y: auto;
`)} `}
`; `;
class Lookup extends React.Component { class Lookup extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.assertCorrectValueType();
this.state = { this.state = {
isModalOpen: false, isModalOpen: false,
lookupSelectedItems: [...props.value] || [], lookupSelectedItems: props.multiple
? [...props.value] || []
: [props.value],
results: [], results: [],
count: 0, count: 0,
error: null, error: null,
@@ -59,6 +72,18 @@ class Lookup extends React.Component {
} }
} }
assertCorrectValueType() {
const { multiple, value } = this.props;
if (!multiple && Array.isArray(value)) {
throw new Error(
'Lookup value must not be an array unless `multiple` is set'
);
}
if (multiple && !Array.isArray(value)) {
throw new Error('Lookup value must be an array if `multiple` is set');
}
}
async getData() { async getData() {
const { const {
getItems, getItems,
@@ -81,7 +106,7 @@ class Lookup extends React.Component {
} }
toggleSelected(row) { toggleSelected(row) {
const { name, onLookupSave } = this.props; const { name, onLookupSave, multiple } = this.props;
const { const {
lookupSelectedItems: updatedSelectedItems, lookupSelectedItems: updatedSelectedItems,
isModalOpen, isModalOpen,
@@ -91,13 +116,17 @@ class Lookup extends React.Component {
selectedRow => selectedRow.id === row.id selectedRow => selectedRow.id === row.id
); );
if (selectedIndex > -1) { if (multiple) {
updatedSelectedItems.splice(selectedIndex, 1); if (selectedIndex > -1) {
this.setState({ lookupSelectedItems: updatedSelectedItems }); updatedSelectedItems.splice(selectedIndex, 1);
this.setState({ lookupSelectedItems: updatedSelectedItems });
} else {
this.setState(prevState => ({
lookupSelectedItems: [...prevState.lookupSelectedItems, row],
}));
}
} else { } else {
this.setState(prevState => ({ this.setState({ lookupSelectedItems: [row] });
lookupSelectedItems: [...prevState.lookupSelectedItems, row],
}));
} }
// Updates the selected items from parent state // Updates the selected items from parent state
@@ -110,12 +139,14 @@ class Lookup extends React.Component {
handleModalToggle() { handleModalToggle() {
const { isModalOpen } = this.state; const { isModalOpen } = this.state;
const { value } = this.props; const { value, multiple } = this.props;
// Resets the selected items from parent state whenever modal is opened // Resets the selected items from parent state whenever modal is opened
// This handles the case where the user closes/cancels the modal and // This handles the case where the user closes/cancels the modal and
// opens it again // opens it again
if (!isModalOpen) { if (!isModalOpen) {
this.setState({ lookupSelectedItems: [...value] }); this.setState({
lookupSelectedItems: multiple ? [...value] : [value],
});
} }
this.setState(prevState => ({ this.setState(prevState => ({
isModalOpen: !prevState.isModalOpen, isModalOpen: !prevState.isModalOpen,
@@ -123,9 +154,10 @@ class Lookup extends React.Component {
} }
saveModal() { saveModal() {
const { onLookupSave, name } = this.props; const { onLookupSave, name, multiple } = this.props;
const { lookupSelectedItems } = this.state; const { lookupSelectedItems } = this.state;
onLookupSave(lookupSelectedItems, name); const value = multiple ? lookupSelectedItems : lookupSelectedItems[0];
onLookupSave(value, name);
this.handleModalToggle(); this.handleModalToggle();
} }
@@ -137,13 +169,21 @@ class Lookup extends React.Component {
results, results,
count, count,
} = this.state; } = this.state;
const { id, lookupHeader, value, columns, i18n } = this.props; const {
id,
lookupHeader,
value,
columns,
multiple,
name,
i18n,
} = this.props;
const header = lookupHeader || i18n._(t`items`); const header = lookupHeader || i18n._(t`items`);
const chips = value ? ( const chips = value ? (
<ChipGroup> <ChipGroup>
{value.map(chip => ( {(multiple ? value : [value]).map(chip => (
<Chip key={chip.id} onClick={() => this.toggleSelected(chip)}> <Chip key={chip.id} onClick={() => this.toggleSelected(chip)}>
{chip.name} {chip.name}
</Chip> </Chip>
@@ -197,9 +237,11 @@ class Lookup extends React.Component {
<CheckboxListItem <CheckboxListItem
key={item.id} key={item.id}
itemId={item.id} itemId={item.id}
name={item.name} name={multiple ? item.name : name}
label={item.name}
isSelected={lookupSelectedItems.some(i => i.id === item.id)} isSelected={lookupSelectedItems.some(i => i.id === item.id)}
onSelect={() => this.toggleSelected(item)} onSelect={() => this.toggleSelected(item)}
isRadio={!multiple}
/> />
)} )}
renderToolbar={props => <DataListToolbar {...props} fillWidth />} renderToolbar={props => <DataListToolbar {...props} fillWidth />}
@@ -220,15 +262,19 @@ class Lookup extends React.Component {
} }
} }
const Item = shape({
id: number.isRequired,
});
Lookup.propTypes = { Lookup.propTypes = {
id: PropTypes.string, id: string,
getItems: PropTypes.func.isRequired, getItems: func.isRequired,
lookupHeader: PropTypes.string, lookupHeader: string,
name: PropTypes.string, // TODO: delete, unused ? name: string,
onLookupSave: PropTypes.func.isRequired, onLookupSave: func.isRequired,
value: PropTypes.arrayOf(PropTypes.object).isRequired, value: oneOfType([Item, arrayOf(Item)]).isRequired,
sortedColumnKey: PropTypes.string.isRequired, sortedColumnKey: string.isRequired,
multiple: PropTypes.bool, multiple: bool,
}; };
Lookup.defaultProps = { Lookup.defaultProps = {

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import { string, func } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { FormGroup, Tooltip } from '@patternfly/react-core'; import { FormGroup, Tooltip } from '@patternfly/react-core';
@@ -7,6 +7,7 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
import { InventoriesAPI } from '@api'; import { InventoriesAPI } from '@api';
import Lookup from '@components/Lookup'; import Lookup from '@components/Lookup';
import { Inventory } from '../../../types';
const getInventories = async params => InventoriesAPI.read(params); const getInventories = async params => InventoriesAPI.read(params);
@@ -35,7 +36,6 @@ class InventoriesLookup extends React.Component {
value={value} value={value}
onLookupSave={onChange} onLookupSave={onChange}
getItems={getInventories} getItems={getInventories}
multiple
columns={[ columns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true }, { name: i18n._(t`Name`), key: 'name', isSortable: true },
{ {
@@ -59,9 +59,9 @@ class InventoriesLookup extends React.Component {
} }
InventoriesLookup.propTypes = { InventoriesLookup.propTypes = {
value: PropTypes.arrayOf(PropTypes.object).isRequired, value: Inventory.isRequired,
tooltip: PropTypes.string, tooltip: string,
onChange: PropTypes.func.isRequired, onChange: func.isRequired,
}; };
InventoriesLookup.defaultProps = { InventoriesLookup.defaultProps = {

View File

@@ -10,10 +10,10 @@ import AnsibleSelect from '@components/AnsibleSelect';
import FormActionGroup from '@components/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup';
import FormField from '@components/FormField'; import FormField from '@components/FormField';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import Lookup from '@components/Lookup';
import { required } from '@util/validators'; import { required } from '@util/validators';
import styled from 'styled-components'; import styled from 'styled-components';
import { JobTemplate } from '@types'; import { JobTemplate } from '@types';
import InventoriesLookup from './InventoriesLookup';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)` const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px; margin-left: 10px;
@@ -34,11 +34,21 @@ class JobTemplateForm extends Component {
job_type: 'run', job_type: 'run',
project: '', project: '',
playbook: '', playbook: '',
summary_fields: {},
}, },
}; };
constructor(props) {
super(props);
this.state = {
inventory: props.template.summary_fields.inventory,
};
}
render() { render() {
const { handleCancel, handleSubmit, i18n, template } = this.props; const { handleCancel, handleSubmit, i18n, template } = this.props;
const { inventory } = this.state;
const jobTypeOptions = [ const jobTypeOptions = [
{ {
@@ -109,8 +119,19 @@ class JobTemplateForm extends Component {
<Field <Field
name="inventory" name="inventory"
validate={required(null, i18n)} validate={required(null, i18n)}
render={({ field, form }) => ( render={({ form }) => (
<FormGroup <InventoriesLookup
value={inventory}
tooltip={i18n._(t`Select the inventory containing the hosts
you want this job to manage.`)}
onChange={value => {
form.setFieldValue('inventory', value.id);
this.setState({ inventory: value });
}}
/>
)}
/>
{/* <FormGroup
fieldId="template-inventory" fieldId="template-inventory"
helperTextInvalid={form.errors.inventory} helperTextInvalid={form.errors.inventory}
isRequired isRequired
@@ -124,8 +145,8 @@ class JobTemplateForm extends Component {
onLookupSave={(value) => {console.log(value)}} onLookupSave={(value) => {console.log(value)}}
getItems={() => ({ getItems={() => ({
data: { data: {
results: [{id: 1, name: 'foo'}], results: [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}],
count: 1 count: 2
} }
})} })}
columns={[ columns={[
@@ -133,9 +154,7 @@ class JobTemplateForm extends Component {
]} ]}
sortedColumnsKey="name" sortedColumnsKey="name"
/> />
</FormGroup> </FormGroup> */}
)}
/>
{/* <FormField {/* <FormField
id="template-inventory" id="template-inventory"
name="inventory" name="inventory"

View File

@@ -123,3 +123,19 @@ export const Job = shape({
extra_vars: string, extra_vars: string,
artifacts: shape({}), artifacts: shape({}),
}); });
export const Inventory = shape({
id: number.isRequired,
description: string,
groups_with_active_failures: number,
has_active_failures: bool,
has_inventory_sources: bool,
hosts_with_active_failures: number,
inventory_sources_with_failures: number,
kind: string,
name: string,
organization_id: number,
total_groups: number,
total_hosts: number,
total_inventory_sources: number,
});