mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
convert all lookups to use new Lookup component
This commit is contained in:
parent
639b297027
commit
cb07e9c757
@ -38,7 +38,7 @@ class AnsibleSelect extends React.Component {
|
||||
>
|
||||
{data.map(option => (
|
||||
<FormSelectOption
|
||||
key={option.id}
|
||||
key={option.key}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
isDisabled={option.isDisabled}
|
||||
@ -50,7 +50,7 @@ class AnsibleSelect extends React.Component {
|
||||
}
|
||||
|
||||
const Option = shape({
|
||||
id: oneOfType([string, number]).isRequired,
|
||||
key: oneOfType([string, number]).isRequired,
|
||||
value: oneOfType([string, number]).isRequired,
|
||||
label: string.isRequired,
|
||||
isDisabled: bool,
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { bool, func, number, string, oneOfType } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CredentialsAPI } from '@api';
|
||||
import { Credential } from '@types';
|
||||
import { mergeParams } from '@util/qs';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '@util/qs';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import Lookup from '@components/Lookup';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||
|
||||
const QS_CONFIG = getQSConfig('credentials', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function CredentialLookup({
|
||||
helperTextInvalid,
|
||||
@ -16,11 +25,28 @@ function CredentialLookup({
|
||||
required,
|
||||
credentialTypeId,
|
||||
value,
|
||||
history,
|
||||
}) {
|
||||
const getCredentials = async params =>
|
||||
CredentialsAPI.read(
|
||||
mergeParams(params, { credential_type: credentialTypeId })
|
||||
);
|
||||
const [credentials, setCredentials] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
try {
|
||||
const { data } = await CredentialsAPI.read(
|
||||
mergeParams(params, { credential_type: credentialTypeId })
|
||||
);
|
||||
setCredentials(data.results);
|
||||
setCount(data.count);
|
||||
} catch (err) {
|
||||
if (setError) {
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
@ -32,15 +58,25 @@ function CredentialLookup({
|
||||
>
|
||||
<Lookup
|
||||
id="credential"
|
||||
lookupHeader={label}
|
||||
name="credential"
|
||||
header={label}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
getItems={getCredentials}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={credentials}
|
||||
optionCount={count}
|
||||
header={label}
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={canDelete}
|
||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<LookupErrorMessage error={error} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
@ -65,4 +101,4 @@ CredentialLookup.defaultProps = {
|
||||
};
|
||||
|
||||
export { CredentialLookup as _CredentialLookup };
|
||||
export default withI18n()(CredentialLookup);
|
||||
export default withI18n()(withRouter(CredentialLookup));
|
||||
|
||||
@ -7,8 +7,9 @@ import { FormGroup } from '@patternfly/react-core';
|
||||
import { InstanceGroupsAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import { FieldTooltip } from '@components/FormField';
|
||||
import Lookup from './NewLookup';
|
||||
import SelectList from './shared/SelectList';
|
||||
import Lookup from './Lookup';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||
|
||||
const QS_CONFIG = getQSConfig('instance_groups', {
|
||||
page: 1,
|
||||
@ -45,17 +46,12 @@ function InstanceGroupsLookup(props) {
|
||||
<Lookup
|
||||
id="org-instance-groups"
|
||||
header={i18n._(t`Instance Groups`)}
|
||||
// name="instanceGroups"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
// items={instanceGroups}
|
||||
// count={count}
|
||||
qsConfig={QS_CONFIG}
|
||||
multiple
|
||||
// columns={}
|
||||
sortedColumnKey="name"
|
||||
renderSelectList={({ state, dispatch, canDelete }) => (
|
||||
<SelectList
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={instanceGroups}
|
||||
optionCount={count}
|
||||
@ -89,7 +85,7 @@ function InstanceGroupsLookup(props) {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{error ? <div>error {error.message}</div> : ''}
|
||||
<LookupErrorMessage error={error} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { string, func, bool } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
@ -7,61 +8,93 @@ import { InventoriesAPI } from '@api';
|
||||
import { Inventory } from '@types';
|
||||
import Lookup from '@components/Lookup';
|
||||
import { FieldTooltip } from '@components/FormField';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||
|
||||
const getInventories = async params => InventoriesAPI.read(params);
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
class InventoryLookup extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
tooltip,
|
||||
onChange,
|
||||
onBlur,
|
||||
required,
|
||||
isValid,
|
||||
helperTextInvalid,
|
||||
i18n,
|
||||
} = this.props;
|
||||
function InventoryLookup({
|
||||
value,
|
||||
tooltip,
|
||||
onChange,
|
||||
onBlur,
|
||||
required,
|
||||
isValid,
|
||||
helperTextInvalid,
|
||||
i18n,
|
||||
history,
|
||||
}) {
|
||||
const [inventories, setInventories] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n._(t`Inventory`)}
|
||||
isRequired={required}
|
||||
fieldId="inventory-lookup"
|
||||
isValid={isValid}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
>
|
||||
{tooltip && <FieldTooltip content={tooltip} />}
|
||||
<Lookup
|
||||
id="inventory-lookup"
|
||||
lookupHeader={i18n._(t`Inventory`)}
|
||||
name="inventory"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
getItems={getInventories}
|
||||
required={required}
|
||||
qsNamespace="inventory"
|
||||
columns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
sortedColumnKey="name"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
try {
|
||||
const { data } = await InventoriesAPI.read(params);
|
||||
setInventories(data.results);
|
||||
setCount(data.count);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
})();
|
||||
}, [history.location]);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n._(t`Inventory`)}
|
||||
isRequired={required}
|
||||
fieldId="inventory-lookup"
|
||||
isValid={isValid}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
>
|
||||
{tooltip && <FieldTooltip content={tooltip} />}
|
||||
<Lookup
|
||||
id="inventory-lookup"
|
||||
header={i18n._(t`Inventory`)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={inventories}
|
||||
optionCount={count}
|
||||
columns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
{
|
||||
name: i18n._(t`Modified`),
|
||||
key: 'modified',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created`),
|
||||
key: 'created',
|
||||
isSortable: false,
|
||||
isNumeric: true,
|
||||
},
|
||||
]}
|
||||
multiple={state.multiple}
|
||||
header={i18n._(t`Inventory`)}
|
||||
name="inventory"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={!canDelete}
|
||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<LookupErrorMessage error={error} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryLookup.propTypes = {
|
||||
@ -77,4 +110,4 @@ InventoryLookup.defaultProps = {
|
||||
required: false,
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryLookup);
|
||||
export default withI18n()(withRouter(InventoryLookup));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useReducer, useEffect } from 'react';
|
||||
import {
|
||||
string,
|
||||
bool,
|
||||
@ -20,7 +20,7 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import SelectList from './shared/SelectList';
|
||||
import reducer, { initReducer } from './shared/reducer';
|
||||
import { ChipGroup, Chip } from '../Chip';
|
||||
import { QSConfig } from '@types';
|
||||
|
||||
@ -48,198 +48,118 @@ const ChipHolder = styled.div`
|
||||
border-bottom-right-radius: 3px;
|
||||
`;
|
||||
|
||||
class Lookup extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function Lookup(props) {
|
||||
const {
|
||||
id,
|
||||
header,
|
||||
onChange,
|
||||
onBlur,
|
||||
value,
|
||||
multiple,
|
||||
required,
|
||||
qsConfig,
|
||||
renderItemChip,
|
||||
renderOptionsList,
|
||||
history,
|
||||
i18n,
|
||||
} = props;
|
||||
|
||||
this.assertCorrectValueType();
|
||||
let selectedItems = [];
|
||||
if (props.value) {
|
||||
selectedItems = props.multiple ? [...props.value] : [props.value];
|
||||
}
|
||||
this.state = {
|
||||
isModalOpen: false,
|
||||
selectedItems,
|
||||
};
|
||||
this.handleModalToggle = this.handleModalToggle.bind(this);
|
||||
this.addItem = this.addItem.bind(this);
|
||||
this.removeItem = this.removeItem.bind(this);
|
||||
this.saveModal = this.saveModal.bind(this);
|
||||
this.clearQSParams = this.clearQSParams.bind(this);
|
||||
}
|
||||
const [state, dispatch] = useReducer(
|
||||
reducer,
|
||||
{ value, multiple, required },
|
||||
initReducer
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_MULTIPLE', value: multiple });
|
||||
}, [multiple]);
|
||||
|
||||
removeItem(row) {
|
||||
const { selectedItems } = this.state;
|
||||
const { onToggleItem } = this.props;
|
||||
if (onToggleItem) {
|
||||
this.setState({ selectedItems: onToggleItem(selectedItems, row) });
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedItems: selectedItems.filter(item => item.id !== row.id),
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_VALUE', value });
|
||||
}, [value]);
|
||||
|
||||
addItem(row) {
|
||||
const { selectedItems } = this.state;
|
||||
const { multiple, onToggleItem } = this.props;
|
||||
if (onToggleItem) {
|
||||
this.setState({ selectedItems: onToggleItem(selectedItems, row) });
|
||||
return;
|
||||
}
|
||||
const index = selectedItems.findIndex(item => item.id === row.id);
|
||||
|
||||
if (!multiple) {
|
||||
this.setState({ selectedItems: [row] });
|
||||
return;
|
||||
}
|
||||
if (index > -1) {
|
||||
return;
|
||||
}
|
||||
this.setState({ selectedItems: [...selectedItems, row] });
|
||||
}
|
||||
|
||||
// TODO: cleanup
|
||||
handleModalToggle() {
|
||||
const { isModalOpen } = this.state;
|
||||
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) {
|
||||
let selectedItems = [];
|
||||
if (value) {
|
||||
selectedItems = multiple ? [...value] : [value];
|
||||
}
|
||||
this.setState({ selectedItems });
|
||||
} else {
|
||||
this.clearQSParams();
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
isModalOpen: !prevState.isModalOpen,
|
||||
}));
|
||||
}
|
||||
|
||||
removeItemAndSave(item) {
|
||||
const { value, onChange, multiple } = this.props;
|
||||
if (multiple) {
|
||||
onChange(value.filter(i => i.id !== item.id));
|
||||
} else if (value.id === item.id) {
|
||||
onChange(null);
|
||||
}
|
||||
}
|
||||
|
||||
saveModal() {
|
||||
const { onChange, multiple } = this.props;
|
||||
const { selectedItems } = this.state;
|
||||
const value = multiple ? selectedItems : selectedItems[0] || null;
|
||||
|
||||
this.handleModalToggle();
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
clearQSParams() {
|
||||
const { qsConfig, history } = this.props;
|
||||
const clearQSParams = () => {
|
||||
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||
const ns = qsConfig.namespace;
|
||||
const otherParts = parts.filter(param => !param.startsWith(`${ns}.`));
|
||||
history.push(`${history.location.pathname}?${otherParts.join('&')}`);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isModalOpen, selectedItems } = this.state;
|
||||
const {
|
||||
id,
|
||||
lookupHeader,
|
||||
value,
|
||||
items,
|
||||
count,
|
||||
columns,
|
||||
multiple,
|
||||
name,
|
||||
onBlur,
|
||||
required,
|
||||
qsConfig,
|
||||
i18n,
|
||||
} = this.props;
|
||||
const header = lookupHeader || i18n._(t`Items`);
|
||||
const canDelete = !required || (multiple && value.length > 1);
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup onBlur={onBlur}>
|
||||
<SearchButton
|
||||
aria-label="Search"
|
||||
id={id}
|
||||
onClick={this.handleModalToggle}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
<SearchIcon />
|
||||
</SearchButton>
|
||||
<ChipHolder className="pf-c-form-control">
|
||||
<ChipGroup defaultIsOpen numChips={5}>
|
||||
{(multiple ? value : [value]).map(chip => (
|
||||
<Chip
|
||||
key={chip.id}
|
||||
onClick={() => this.removeItemAndSave(chip)}
|
||||
isReadOnly={!canDelete}
|
||||
>
|
||||
{chip.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
</InputGroup>
|
||||
<Modal
|
||||
className="awx-c-modal"
|
||||
title={i18n._(t`Select ${header}`)}
|
||||
isOpen={isModalOpen}
|
||||
onClose={this.handleModalToggle}
|
||||
actions={[
|
||||
<Button
|
||||
key="select"
|
||||
variant="primary"
|
||||
onClick={this.saveModal}
|
||||
style={selectedItems.length === 0 ? { display: 'none' } : {}}
|
||||
>
|
||||
{i18n._(t`Select`)}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
onClick={this.handleModalToggle}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
const save = () => {
|
||||
const { selectedItems } = state;
|
||||
const val = multiple ? selectedItems : selectedItems[0] || null;
|
||||
onChange(val);
|
||||
clearQSParams();
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const removeItem = item => {
|
||||
if (multiple) {
|
||||
onChange(value.filter(i => i.id !== item.id));
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
clearQSParams();
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const { isModalOpen, selectedItems } = state;
|
||||
const canDelete = !required || (multiple && value.length > 1);
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup onBlur={onBlur}>
|
||||
<SearchButton
|
||||
aria-label="Search"
|
||||
id={id}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
<SelectList
|
||||
value={selectedItems}
|
||||
onChange={newVal => this.setState({ selectedItems: newVal })}
|
||||
options={items}
|
||||
optionCount={count}
|
||||
columns={columns}
|
||||
multiple={multiple}
|
||||
header={lookupHeader}
|
||||
name={name}
|
||||
qsConfig={qsConfig}
|
||||
readOnly={!canDelete}
|
||||
/>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
<SearchIcon />
|
||||
</SearchButton>
|
||||
<ChipHolder className="pf-c-form-control">
|
||||
<ChipGroup>
|
||||
{(multiple ? value : [value]).map(item =>
|
||||
renderItemChip({
|
||||
item,
|
||||
removeItem,
|
||||
canDelete,
|
||||
})
|
||||
)}
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
</InputGroup>
|
||||
<Modal
|
||||
className="awx-c-modal"
|
||||
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
actions={[
|
||||
<Button
|
||||
key="select"
|
||||
variant="primary"
|
||||
onClick={save}
|
||||
style={
|
||||
required && selectedItems.length === 0 ? { display: 'none' } : {}
|
||||
}
|
||||
>
|
||||
{i18n._(t`Select`)}
|
||||
</Button>,
|
||||
<Button key="cancel" variant="secondary" onClick={closeModal}>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{renderOptionsList({
|
||||
state,
|
||||
dispatch,
|
||||
canDelete,
|
||||
})}
|
||||
</Modal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const Item = shape({
|
||||
@ -248,25 +168,33 @@ const Item = shape({
|
||||
|
||||
Lookup.propTypes = {
|
||||
id: string,
|
||||
items: arrayOf(shape({})).isRequired,
|
||||
count: number.isRequired,
|
||||
// TODO: change to `header`
|
||||
lookupHeader: string,
|
||||
name: string,
|
||||
header: string,
|
||||
onChange: func.isRequired,
|
||||
value: oneOfType([Item, arrayOf(Item)]),
|
||||
multiple: bool,
|
||||
required: bool,
|
||||
onBlur: func,
|
||||
qsConfig: QSConfig.isRequired,
|
||||
renderItemChip: func,
|
||||
renderOptionsList: func.isRequired,
|
||||
};
|
||||
|
||||
Lookup.defaultProps = {
|
||||
id: 'lookup-search',
|
||||
lookupHeader: null,
|
||||
name: null,
|
||||
header: null,
|
||||
value: null,
|
||||
multiple: false,
|
||||
required: false,
|
||||
onBlur: () => {},
|
||||
renderItemChip: ({ item, removeItem, canDelete }) => (
|
||||
<Chip
|
||||
key={item.id}
|
||||
onClick={() => removeItem(item)}
|
||||
isReadOnly={!canDelete}
|
||||
>
|
||||
{item.name}
|
||||
</Chip>
|
||||
),
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
|
||||
@ -10,8 +10,8 @@ import { FieldTooltip } from '@components/FormField';
|
||||
import { CredentialChip } from '@components/Chip';
|
||||
import VerticalSeperator from '@components/VerticalSeparator';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import Lookup from './NewLookup';
|
||||
import SelectList from './shared/SelectList';
|
||||
import Lookup from './Lookup';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
|
||||
const QS_CONFIG = getQSConfig('credentials', {
|
||||
page: 1,
|
||||
@ -37,7 +37,6 @@ function MultiCredentialsLookup(props) {
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const [credentials, setCredentials] = useState([]);
|
||||
const [credentialsCount, setCredentialsCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -60,12 +59,10 @@ function MultiCredentialsLookup(props) {
|
||||
}
|
||||
try {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
setIsLoading(true);
|
||||
const { results, count } = await loadCredentials(
|
||||
params,
|
||||
selectedType.id
|
||||
);
|
||||
setIsLoading(false);
|
||||
setCredentials(results);
|
||||
setCredentialsCount(count);
|
||||
} catch (err) {
|
||||
@ -74,7 +71,7 @@ function MultiCredentialsLookup(props) {
|
||||
})();
|
||||
}, [selectedType]);
|
||||
|
||||
const isMultiple = selectedType && selectedType.value === 'Vault';
|
||||
const isMultiple = selectedType && selectedType.name === 'Vault';
|
||||
const renderChip = ({ item, removeItem, canDelete }) => (
|
||||
<CredentialChip
|
||||
key={item.id}
|
||||
@ -95,7 +92,7 @@ function MultiCredentialsLookup(props) {
|
||||
onChange={onChange}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItemChip={renderChip}
|
||||
renderSelectList={({ state, dispatch, canDelete }) => {
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{credentialTypes && credentialTypes.length > 0 && (
|
||||
@ -107,7 +104,7 @@ function MultiCredentialsLookup(props) {
|
||||
id="multiCredentialsLookUp-select"
|
||||
label={i18n._(t`Selected Category`)}
|
||||
data={credentialTypes.map(type => ({
|
||||
id: type.id,
|
||||
key: type.id,
|
||||
value: type.id,
|
||||
label: type.name,
|
||||
isDisabled: false,
|
||||
@ -121,7 +118,7 @@ function MultiCredentialsLookup(props) {
|
||||
/>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
<SelectList
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={credentials}
|
||||
optionCount={credentialsCount}
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
import React, { Fragment, useReducer, useEffect } from 'react';
|
||||
import {
|
||||
string,
|
||||
bool,
|
||||
arrayOf,
|
||||
func,
|
||||
number,
|
||||
oneOfType,
|
||||
shape,
|
||||
} from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { SearchIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
InputGroup as PFInputGroup,
|
||||
Modal,
|
||||
} from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import reducer, { initReducer } from './shared/reducer';
|
||||
import { ChipGroup, Chip } from '../Chip';
|
||||
import { QSConfig } from '@types';
|
||||
|
||||
const SearchButton = styled(Button)`
|
||||
::after {
|
||||
border: var(--pf-c-button--BorderWidth) solid
|
||||
var(--pf-global--BorderColor--200);
|
||||
}
|
||||
`;
|
||||
|
||||
const InputGroup = styled(PFInputGroup)`
|
||||
${props =>
|
||||
props.multiple &&
|
||||
`
|
||||
--pf-c-form-control--Height: 90px;
|
||||
overflow-y: auto;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ChipHolder = styled.div`
|
||||
--pf-c-form-control--BorderTopColor: var(--pf-global--BorderColor--200);
|
||||
--pf-c-form-control--BorderRightColor: var(--pf-global--BorderColor--200);
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
`;
|
||||
|
||||
function Lookup(props) {
|
||||
const {
|
||||
id,
|
||||
header,
|
||||
onChange,
|
||||
onBlur,
|
||||
value,
|
||||
multiple,
|
||||
required,
|
||||
qsConfig,
|
||||
renderItemChip,
|
||||
renderSelectList,
|
||||
history,
|
||||
i18n,
|
||||
} = props;
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
reducer,
|
||||
{ value, multiple, required },
|
||||
initReducer
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_MULTIPLE', value: multiple });
|
||||
}, [multiple]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'SET_VALUE', value });
|
||||
}, [value]);
|
||||
|
||||
const clearQSParams = () => {
|
||||
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||
const ns = qsConfig.namespace;
|
||||
const otherParts = parts.filter(param => !param.startsWith(`${ns}.`));
|
||||
history.push(`${history.location.pathname}?${otherParts.join('&')}`);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
const { selectedItems } = state;
|
||||
const val = multiple ? selectedItems : selectedItems[0] || null;
|
||||
onChange(val);
|
||||
clearQSParams();
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const removeItem = item => {
|
||||
if (multiple) {
|
||||
onChange(value.filter(i => i.id !== item.id));
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
clearQSParams();
|
||||
dispatch({ type: 'CLOSE_MODAL' });
|
||||
};
|
||||
|
||||
const { isModalOpen, selectedItems } = state;
|
||||
const canDelete = !required || (multiple && value.length > 1);
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup onBlur={onBlur}>
|
||||
<SearchButton
|
||||
aria-label="Search"
|
||||
id={id}
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
<SearchIcon />
|
||||
</SearchButton>
|
||||
<ChipHolder className="pf-c-form-control">
|
||||
<ChipGroup>
|
||||
{(multiple ? value : [value]).map(item =>
|
||||
renderItemChip({
|
||||
item,
|
||||
removeItem,
|
||||
canDelete,
|
||||
})
|
||||
)}
|
||||
</ChipGroup>
|
||||
</ChipHolder>
|
||||
</InputGroup>
|
||||
<Modal
|
||||
className="awx-c-modal"
|
||||
title={i18n._(t`Select ${header || i18n._(t`Items`)}`)}
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
actions={[
|
||||
<Button
|
||||
key="select"
|
||||
variant="primary"
|
||||
onClick={save}
|
||||
style={
|
||||
required && selectedItems.length === 0 ? { display: 'none' } : {}
|
||||
}
|
||||
>
|
||||
{i18n._(t`Select`)}
|
||||
</Button>,
|
||||
<Button key="cancel" variant="secondary" onClick={closeModal}>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{renderSelectList({
|
||||
state,
|
||||
dispatch,
|
||||
canDelete,
|
||||
})}
|
||||
</Modal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const Item = shape({
|
||||
id: number.isRequired,
|
||||
});
|
||||
|
||||
Lookup.propTypes = {
|
||||
id: string,
|
||||
header: string,
|
||||
onChange: func.isRequired,
|
||||
value: oneOfType([Item, arrayOf(Item)]),
|
||||
multiple: bool,
|
||||
required: bool,
|
||||
onBlur: func,
|
||||
qsConfig: QSConfig.isRequired,
|
||||
renderItemChip: func,
|
||||
renderSelectList: func.isRequired,
|
||||
};
|
||||
|
||||
Lookup.defaultProps = {
|
||||
id: 'lookup-search',
|
||||
header: null,
|
||||
value: null,
|
||||
multiple: false,
|
||||
required: false,
|
||||
onBlur: () => {},
|
||||
renderItemChip: ({ item, removeItem, canDelete }) => (
|
||||
<Chip
|
||||
key={item.id}
|
||||
onClick={() => removeItem(item)}
|
||||
isReadOnly={!canDelete}
|
||||
>
|
||||
{item.name}
|
||||
</Chip>
|
||||
),
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
export default withI18n()(withRouter(Lookup));
|
||||
@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { string, func, bool } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { string, func, bool } from 'prop-types';
|
||||
import { OrganizationsAPI } from '@api';
|
||||
import { Organization } from '@types';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import Lookup from '@components/Lookup';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import Lookup from './Lookup';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||
|
||||
const getOrganizations = async params => OrganizationsAPI.read(params);
|
||||
const QS_CONFIG = getQSConfig('organizations', {});
|
||||
|
||||
function OrganizationLookup({
|
||||
helperTextInvalid,
|
||||
@ -17,7 +21,25 @@ function OrganizationLookup({
|
||||
onChange,
|
||||
required,
|
||||
value,
|
||||
history,
|
||||
}) {
|
||||
const [organizations, setOrganizations] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
try {
|
||||
const { data } = await OrganizationsAPI.read(params);
|
||||
setOrganizations(data.results);
|
||||
setCount(data.count);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
})();
|
||||
}, [history.location]);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="organization"
|
||||
@ -28,15 +50,29 @@ function OrganizationLookup({
|
||||
>
|
||||
<Lookup
|
||||
id="organization"
|
||||
lookupHeader={i18n._(t`Organization`)}
|
||||
name="organization"
|
||||
header={i18n._(t`Organization`)}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
getItems={getOrganizations}
|
||||
qsConfig={QS_CONFIG}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={organizations}
|
||||
optionCount={count}
|
||||
multiple={state.multiple}
|
||||
header={i18n._(t`Organization`)}
|
||||
name="organization"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={!canDelete}
|
||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<LookupErrorMessage error={error} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
@ -58,5 +94,5 @@ OrganizationLookup.defaultProps = {
|
||||
value: null,
|
||||
};
|
||||
|
||||
export default withI18n()(OrganizationLookup);
|
||||
export { OrganizationLookup as _OrganizationLookup };
|
||||
export default withI18n()(withRouter(OrganizationLookup));
|
||||
|
||||
@ -1,59 +1,87 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { string, func, bool } from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { ProjectsAPI } from '@api';
|
||||
import { Project } from '@types';
|
||||
import Lookup from '@components/Lookup';
|
||||
import { FieldTooltip } from '@components/FormField';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import Lookup from './Lookup';
|
||||
import OptionsList from './shared/OptionsList';
|
||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||
|
||||
class ProjectLookup extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
helperTextInvalid,
|
||||
i18n,
|
||||
isValid,
|
||||
onChange,
|
||||
required,
|
||||
tooltip,
|
||||
value,
|
||||
onBlur,
|
||||
} = this.props;
|
||||
const QS_CONFIG = getQSConfig('project', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
const loadProjects = async params => {
|
||||
const response = await ProjectsAPI.read(params);
|
||||
const { results, count } = response.data;
|
||||
if (count === 1) {
|
||||
onChange(results[0], 'project');
|
||||
function ProjectLookup({
|
||||
helperTextInvalid,
|
||||
i18n,
|
||||
isValid,
|
||||
onChange,
|
||||
required,
|
||||
tooltip,
|
||||
value,
|
||||
onBlur,
|
||||
history,
|
||||
}) {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
try {
|
||||
const { data } = await ProjectsAPI.read(params);
|
||||
setProjects(data.results);
|
||||
setCount(data.count);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
})();
|
||||
}, [history.location]);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="project"
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
isRequired={required}
|
||||
isValid={isValid}
|
||||
label={i18n._(t`Project`)}
|
||||
>
|
||||
{tooltip && <FieldTooltip content={tooltip} />}
|
||||
<Lookup
|
||||
id="project"
|
||||
lookupHeader={i18n._(t`Project`)}
|
||||
name="project"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
getItems={loadProjects}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
qsNamespace="project"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="project"
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
isRequired={required}
|
||||
isValid={isValid}
|
||||
label={i18n._(t`Project`)}
|
||||
>
|
||||
{tooltip && <FieldTooltip content={tooltip} />}
|
||||
<Lookup
|
||||
id="project"
|
||||
lookupHeader={i18n._(t`Project`)}
|
||||
name="project"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
required={required}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||
<OptionsList
|
||||
value={state.selectedItems}
|
||||
options={projects}
|
||||
optionCount={count}
|
||||
multiple={state.multiple}
|
||||
header={i18n._(t`Project`)}
|
||||
name="project"
|
||||
qsConfig={QS_CONFIG}
|
||||
readOnly={!canDelete}
|
||||
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<LookupErrorMessage error={error} />
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectLookup.propTypes = {
|
||||
@ -75,4 +103,5 @@ ProjectLookup.defaultProps = {
|
||||
onBlur: () => {},
|
||||
};
|
||||
|
||||
export default withI18n()(ProjectLookup);
|
||||
export { ProjectLookup as _ProjectLookup };
|
||||
export default withI18n()(withRouter(ProjectLookup));
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
function LookupErrorMessage({ error }) {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
{error.message || 'An error occured'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LookupErrorMessage;
|
||||
@ -16,7 +16,7 @@ import CheckboxListItem from '../../CheckboxListItem';
|
||||
import DataListToolbar from '../../DataListToolbar';
|
||||
import { QSConfig } from '@types';
|
||||
|
||||
function SelectList({
|
||||
function OptionsList({
|
||||
value,
|
||||
options,
|
||||
optionCount,
|
||||
@ -71,7 +71,7 @@ function SelectList({
|
||||
const Item = shape({
|
||||
id: oneOfType([number, string]).isRequired,
|
||||
});
|
||||
SelectList.propTypes = {
|
||||
OptionsList.propTypes = {
|
||||
value: arrayOf(Item).isRequired,
|
||||
options: arrayOf(Item).isRequired,
|
||||
optionCount: number.isRequired,
|
||||
@ -82,9 +82,9 @@ SelectList.propTypes = {
|
||||
deselectItem: func.isRequired,
|
||||
renderItemChip: func,
|
||||
};
|
||||
SelectList.defaultProps = {
|
||||
OptionsList.defaultProps = {
|
||||
multiple: false,
|
||||
renderItemChip: null,
|
||||
};
|
||||
|
||||
export default withI18n()(SelectList);
|
||||
export default withI18n()(OptionsList);
|
||||
Loading…
x
Reference in New Issue
Block a user