mirror of
https://github.com/ansible/awx.git
synced 2026-01-19 05:31:22 -03:30
finish core InventoriesLookup core functionality
This commit is contained in:
parent
c080346751
commit
5edc6deeae
@ -97,7 +97,7 @@
|
||||
width: 600px;
|
||||
|
||||
.pf-c-modal-box__body {
|
||||
overflow: visible;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pf-c-modal-box__footer > .pf-c-button:not(:last-child) {
|
||||
|
||||
@ -107,6 +107,7 @@ class SelectResourceStep extends React.Component {
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item[displayKey]}
|
||||
label={item[displayKey]}
|
||||
onSelect={() => onRowClick(item)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -7,42 +7,50 @@ import {
|
||||
DataListCheck,
|
||||
DataListCell,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import DataListRadio from '@components/DataListRadio';
|
||||
import VerticalSeparator from '../VerticalSeparator';
|
||||
|
||||
const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => (
|
||||
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`selected-${itemId}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={`check-action-item-${itemId}`}
|
||||
value={itemId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
|
||||
<VerticalSeparator />
|
||||
</DataListCell>,
|
||||
<DataListCell key="name">
|
||||
<label
|
||||
id={`check-action-item-${itemId}`}
|
||||
htmlFor={`selected-${itemId}`}
|
||||
className="check-action-item"
|
||||
const CheckboxListItem = ({ itemId, name, label, isSelected, onSelect, isRadio }) => {
|
||||
const CheckboxRadio = isRadio ? DataListRadio : DataListCheck;
|
||||
return (
|
||||
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
|
||||
<DataListItemRow>
|
||||
<CheckboxRadio
|
||||
id={`selected-${itemId}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={`check-action-item-${itemId}`}
|
||||
name={name}
|
||||
value={itemId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell
|
||||
key="divider"
|
||||
className="pf-c-data-list__cell--divider"
|
||||
>
|
||||
<b>{name}</b>
|
||||
</label>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
<VerticalSeparator />
|
||||
</DataListCell>,
|
||||
<DataListCell key="name">
|
||||
<label
|
||||
id={`check-action-item-${itemId}`}
|
||||
htmlFor={`selected-${itemId}`}
|
||||
className="check-action-item"
|
||||
>
|
||||
<b>{label}</b>
|
||||
</label>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
};
|
||||
|
||||
CheckboxListItem.propTypes = {
|
||||
itemId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ describe('CheckboxListItem', () => {
|
||||
<CheckboxListItem
|
||||
itemId={1}
|
||||
name="Buzz"
|
||||
label="Buzz"
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
|
||||
48
awx/ui_next/src/components/DataListRadio/DataListRadio.jsx
Normal file
48
awx/ui_next/src/components/DataListRadio/DataListRadio.jsx
Normal 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;
|
||||
1
awx/ui_next/src/components/DataListRadio/index.js
Normal file
1
awx/ui_next/src/components/DataListRadio/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './DataListRadio';
|
||||
@ -1,5 +1,13 @@
|
||||
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 { SearchIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
@ -20,19 +28,24 @@ import { ChipGroup, Chip } from '../Chip';
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||
|
||||
const InputGroup = styled(PFInputGroup)`
|
||||
${props => props.multiple && (`
|
||||
${props =>
|
||||
props.multiple &&
|
||||
`
|
||||
--pf-c-form-control--Height: 90px;
|
||||
overflow-y: auto;
|
||||
`)}
|
||||
`}
|
||||
`;
|
||||
|
||||
class Lookup extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.assertCorrectValueType();
|
||||
this.state = {
|
||||
isModalOpen: false,
|
||||
lookupSelectedItems: [...props.value] || [],
|
||||
lookupSelectedItems: props.multiple
|
||||
? [...props.value] || []
|
||||
: [props.value],
|
||||
results: [],
|
||||
count: 0,
|
||||
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() {
|
||||
const {
|
||||
getItems,
|
||||
@ -81,7 +106,7 @@ class Lookup extends React.Component {
|
||||
}
|
||||
|
||||
toggleSelected(row) {
|
||||
const { name, onLookupSave } = this.props;
|
||||
const { name, onLookupSave, multiple } = this.props;
|
||||
const {
|
||||
lookupSelectedItems: updatedSelectedItems,
|
||||
isModalOpen,
|
||||
@ -91,13 +116,17 @@ class Lookup extends React.Component {
|
||||
selectedRow => selectedRow.id === row.id
|
||||
);
|
||||
|
||||
if (selectedIndex > -1) {
|
||||
updatedSelectedItems.splice(selectedIndex, 1);
|
||||
this.setState({ lookupSelectedItems: updatedSelectedItems });
|
||||
if (multiple) {
|
||||
if (selectedIndex > -1) {
|
||||
updatedSelectedItems.splice(selectedIndex, 1);
|
||||
this.setState({ lookupSelectedItems: updatedSelectedItems });
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
lookupSelectedItems: [...prevState.lookupSelectedItems, row],
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
lookupSelectedItems: [...prevState.lookupSelectedItems, row],
|
||||
}));
|
||||
this.setState({ lookupSelectedItems: [row] });
|
||||
}
|
||||
|
||||
// Updates the selected items from parent state
|
||||
@ -110,12 +139,14 @@ class Lookup extends React.Component {
|
||||
|
||||
handleModalToggle() {
|
||||
const { isModalOpen } = this.state;
|
||||
const { value } = this.props;
|
||||
const { value, multiple } = this.props;
|
||||
// Resets the selected items from parent state whenever modal is opened
|
||||
// This handles the case where the user closes/cancels the modal and
|
||||
// opens it again
|
||||
if (!isModalOpen) {
|
||||
this.setState({ lookupSelectedItems: [...value] });
|
||||
this.setState({
|
||||
lookupSelectedItems: multiple ? [...value] : [value],
|
||||
});
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
isModalOpen: !prevState.isModalOpen,
|
||||
@ -123,9 +154,10 @@ class Lookup extends React.Component {
|
||||
}
|
||||
|
||||
saveModal() {
|
||||
const { onLookupSave, name } = this.props;
|
||||
const { onLookupSave, name, multiple } = this.props;
|
||||
const { lookupSelectedItems } = this.state;
|
||||
onLookupSave(lookupSelectedItems, name);
|
||||
const value = multiple ? lookupSelectedItems : lookupSelectedItems[0];
|
||||
onLookupSave(value, name);
|
||||
this.handleModalToggle();
|
||||
}
|
||||
|
||||
@ -137,13 +169,21 @@ class Lookup extends React.Component {
|
||||
results,
|
||||
count,
|
||||
} = 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 chips = value ? (
|
||||
<ChipGroup>
|
||||
{value.map(chip => (
|
||||
{(multiple ? value : [value]).map(chip => (
|
||||
<Chip key={chip.id} onClick={() => this.toggleSelected(chip)}>
|
||||
{chip.name}
|
||||
</Chip>
|
||||
@ -197,9 +237,11 @@ class Lookup extends React.Component {
|
||||
<CheckboxListItem
|
||||
key={item.id}
|
||||
itemId={item.id}
|
||||
name={item.name}
|
||||
name={multiple ? item.name : name}
|
||||
label={item.name}
|
||||
isSelected={lookupSelectedItems.some(i => i.id === item.id)}
|
||||
onSelect={() => this.toggleSelected(item)}
|
||||
isRadio={!multiple}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
@ -220,15 +262,19 @@ class Lookup extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const Item = shape({
|
||||
id: number.isRequired,
|
||||
});
|
||||
|
||||
Lookup.propTypes = {
|
||||
id: PropTypes.string,
|
||||
getItems: PropTypes.func.isRequired,
|
||||
lookupHeader: PropTypes.string,
|
||||
name: PropTypes.string, // TODO: delete, unused ?
|
||||
onLookupSave: PropTypes.func.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortedColumnKey: PropTypes.string.isRequired,
|
||||
multiple: PropTypes.bool,
|
||||
id: string,
|
||||
getItems: func.isRequired,
|
||||
lookupHeader: string,
|
||||
name: string,
|
||||
onLookupSave: func.isRequired,
|
||||
value: oneOfType([Item, arrayOf(Item)]).isRequired,
|
||||
sortedColumnKey: string.isRequired,
|
||||
multiple: bool,
|
||||
};
|
||||
|
||||
Lookup.defaultProps = {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { string, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
||||
@ -7,6 +7,7 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { InventoriesAPI } from '@api';
|
||||
import Lookup from '@components/Lookup';
|
||||
import { Inventory } from '../../../types';
|
||||
|
||||
const getInventories = async params => InventoriesAPI.read(params);
|
||||
|
||||
@ -35,7 +36,6 @@ class InventoriesLookup extends React.Component {
|
||||
value={value}
|
||||
onLookupSave={onChange}
|
||||
getItems={getInventories}
|
||||
multiple
|
||||
columns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
{
|
||||
@ -59,9 +59,9 @@ class InventoriesLookup extends React.Component {
|
||||
}
|
||||
|
||||
InventoriesLookup.propTypes = {
|
||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tooltip: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: Inventory.isRequired,
|
||||
tooltip: string,
|
||||
onChange: func.isRequired,
|
||||
};
|
||||
|
||||
InventoriesLookup.defaultProps = {
|
||||
|
||||
@ -10,10 +10,10 @@ import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import FormActionGroup from '@components/FormActionGroup';
|
||||
import FormField from '@components/FormField';
|
||||
import FormRow from '@components/FormRow';
|
||||
import Lookup from '@components/Lookup';
|
||||
import { required } from '@util/validators';
|
||||
import styled from 'styled-components';
|
||||
import { JobTemplate } from '@types';
|
||||
import InventoriesLookup from './InventoriesLookup';
|
||||
|
||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||
margin-left: 10px;
|
||||
@ -34,11 +34,21 @@ class JobTemplateForm extends Component {
|
||||
job_type: 'run',
|
||||
project: '',
|
||||
playbook: '',
|
||||
summary_fields: {},
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inventory: props.template.summary_fields.inventory,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { handleCancel, handleSubmit, i18n, template } = this.props;
|
||||
const { inventory } = this.state;
|
||||
|
||||
const jobTypeOptions = [
|
||||
{
|
||||
@ -109,8 +119,19 @@ class JobTemplateForm extends Component {
|
||||
<Field
|
||||
name="inventory"
|
||||
validate={required(null, i18n)}
|
||||
render={({ field, form }) => (
|
||||
<FormGroup
|
||||
render={({ form }) => (
|
||||
<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"
|
||||
helperTextInvalid={form.errors.inventory}
|
||||
isRequired
|
||||
@ -124,8 +145,8 @@ class JobTemplateForm extends Component {
|
||||
onLookupSave={(value) => {console.log(value)}}
|
||||
getItems={() => ({
|
||||
data: {
|
||||
results: [{id: 1, name: 'foo'}],
|
||||
count: 1
|
||||
results: [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}],
|
||||
count: 2
|
||||
}
|
||||
})}
|
||||
columns={[
|
||||
@ -133,9 +154,7 @@ class JobTemplateForm extends Component {
|
||||
]}
|
||||
sortedColumnsKey="name"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
/>
|
||||
</FormGroup> */}
|
||||
{/* <FormField
|
||||
id="template-inventory"
|
||||
name="inventory"
|
||||
|
||||
@ -123,3 +123,19 @@ export const Job = shape({
|
||||
extra_vars: string,
|
||||
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,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user