diff --git a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx index 089ec6d969..8071c8b26f 100644 --- a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx @@ -3,32 +3,21 @@ import { arrayOf, string, func, object } from 'prop-types'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { FormGroup, Tooltip } from '@patternfly/react-core'; -import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; -import styled from 'styled-components'; +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'; -const QuestionCircleIcon = styled(PFQuestionCircleIcon)` - margin-left: 10px; -`; - -const QS_CONFIG = getQSConfig('instance-groups', { +const QS_CONFIG = getQSConfig('instance_groups', { page: 1, page_size: 5, order_by: 'name', }); -// const getInstanceGroups = async params => InstanceGroupsAPI.read(params); -function InstanceGroupsLookup({ - value, - onChange, - tooltip, - className, - history, - i18n, -}) { +function InstanceGroupsLookup(props) { + const { value, onChange, tooltip, className, history, i18n } = props; const [instanceGroups, setInstanceGroups] = useState([]); const [count, setCount] = useState(0); const [error, setError] = useState(null); @@ -46,56 +35,62 @@ function InstanceGroupsLookup({ })(); }, [history.location]); - /* - Wrapping
added to workaround PF bug: - https://github.com/patternfly/patternfly-react/issues/2855 - */ return ( -
- - {tooltip && ( - - - + + {tooltip && } + ( + dispatch({ type: 'SELECT_ITEM', item })} + deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} + /> )} - - {error ?
error {error.message}
: ''} -
-
+ /> + {error ?
error {error.message}
: ''} + ); } diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 0277aa0ad3..005c8d620d 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -1,18 +1,18 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { FormGroup, Tooltip } from '@patternfly/react-core'; -import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; -import styled from 'styled-components'; +import { FormGroup, ToolbarItem } from '@patternfly/react-core'; import { CredentialsAPI, CredentialTypesAPI } from '@api'; -import CategoryLookup from '@components/Lookup/CategoryLookup'; +import AnsibleSelect from '@components/AnsibleSelect'; +import { FieldTooltip } from '@components/FormField'; +import { CredentialChip } from '@components/Chip'; +import VerticalSeperator from '@components/VerticalSeparator'; import { getQSConfig, parseQueryString } from '@util/qs'; - -const QuestionCircleIcon = styled(PFQuestionCircleIcon)` - margin-left: 10px; -`; +import Lookup from './NewLookup'; +import SelectList from './shared/SelectList'; +import multiCredentialReducer from './shared/multiCredentialReducer'; const QS_CONFIG = getQSConfig('credentials', { page: 1, @@ -20,6 +20,7 @@ const QS_CONFIG = getQSConfig('credentials', { order_by: 'name', }); +// TODO: move into reducer function toggleCredentialSelection(credentialsToUpdate, newCredential) { let newCredentialsList; const isSelectedCredentialInState = @@ -39,124 +40,164 @@ function toggleCredentialSelection(credentialsToUpdate, newCredential) { return newCredentialsList; } -class MultiCredentialsLookup extends React.Component { - constructor(props) { - super(props); - - this.state = { - selectedCredentialType: { label: 'Machine', id: 1, kind: 'ssh' }, - credentialTypes: [], - }; - this.loadCredentialTypes = this.loadCredentialTypes.bind(this); - this.handleCredentialTypeSelect = this.handleCredentialTypeSelect.bind( - this - ); - this.loadCredentials = this.loadCredentials.bind(this); - } - - componentDidMount() { - this.loadCredentialTypes(); - this.loadCredentials(); - } - - async loadCredentialTypes() { - const { onError } = this.props; - try { - const { data } = await CredentialTypesAPI.read(); - const acceptableTypes = ['machine', 'cloud', 'net', 'ssh', 'vault']; - const credentialTypes = []; - data.results.forEach(cred => { - acceptableTypes.forEach(aT => { - if (aT === cred.kind) { - // This object has several repeated values as some of it's children - // require different field values. - cred = { - id: cred.id, - key: cred.id, - kind: cred.kind, - type: cred.namespace, - value: cred.name, - label: cred.name, - isDisabled: false, - }; - credentialTypes.push(cred); - } - }); - }); - this.setState({ credentialTypes }); - } catch (err) { - onError(err); - } - } - - async loadCredentials() { - const { history, onError } = this.props; - const { selectedCredentialType } = this.state; - const params = parseQueryString(QS_CONFIG, history.location.search); - params.credential_type = selectedCredentialType.id || 1; - try { - const { data } = await CredentialsAPI.read(params); - this.setState({ - credentials: data.results, - count: data.count, - }); - } catch (err) { - onError(err); - } - } - - handleCredentialTypeSelect(value, type) { - const { credentialTypes } = this.state; - const selectedType = credentialTypes.filter(item => item.label === type); - this.setState({ selectedCredentialType: selectedType[0] }, () => { - this.loadCredentials(); +async function loadCredentialTypes() { + const { data } = await CredentialTypesAPI.read(); + const acceptableTypes = ['machine', 'cloud', 'net', 'ssh', 'vault']; + const credentialTypes = []; + // TODO: cleanup + data.results.forEach(cred => { + acceptableTypes.forEach(aT => { + if (aT === cred.kind) { + // This object has several repeated values as some of it's children + // require different field values. + cred = { + id: cred.id, + key: cred.id, + kind: cred.kind, + type: cred.namespace, + value: cred.name, + label: cred.name, + isDisabled: false, + }; + credentialTypes.push(cred); + } }); - } + }); + return credentialTypes; +} - render() { - const { - selectedCredentialType, - credentialTypes, - credentials, - count, - } = this.state; - const { tooltip, i18n, value, onChange } = this.props; - return ( - - {tooltip && ( - - - - )} - {credentialTypes && ( - { + (async () => { + try { + const types = await loadCredentialTypes(); + setCredentialTypes(types); + setSelectedType(types[0]); + } catch (err) { + onError(err); + } + })(); + }, []); + + useEffect(() => { + console.log('useEffect', selectedType); + (async () => { + if (!selectedType) { + return; + } + try { + const params = parseQueryString(QS_CONFIG, history.location.search); + const { results, count } = await loadCredentials( + params, + selectedType.id + ); + setCredentials(results); + setCredentialsCount(count); + } catch (err) { + onError(err); + } + })(); + }, [selectedType]); + + // handleCredentialTypeSelect(value, type) { + // const { credentialTypes } = this.state; + // const selectedType = credentialTypes.filter(item => item.label === type); + // this.setState({ selectedCredentialType: selectedType[0] }, () => { + // this.loadCredentials(); + // }); + // } + + // const { + // selectedCredentialType, + // credentialTypes, + // credentials, + // credentialsCount, + // } = state; + + return ( + + {tooltip && } + ( + removeItem(item)} + isReadOnly={!canDelete} + credential={item} /> )} - - ); - } + renderSelectList={({ state, dispatch, canDelete }) => { + return ( + <> + {credentialTypes && credentialTypes.length > 0 && ( + +
{i18n._(t`Selected Category`)}
+ + { + setSelectedType( + credentialTypes.find(o => o.label === label) + ); + }} + /> +
+ )} + {}} + deselectItem={() => {}} + /> + + ); + }} + /> +
+ ); } MultiCredentialsLookup.propTypes = { @@ -178,6 +219,6 @@ MultiCredentialsLookup.defaultProps = { tooltip: '', value: [], }; -export { MultiCredentialsLookup as _MultiCredentialsLookup }; +export { MultiCredentialsLookup as _MultiCredentialsLookup }; export default withI18n()(withRouter(MultiCredentialsLookup)); diff --git a/awx/ui_next/src/components/Lookup/NewLookup.jsx b/awx/ui_next/src/components/Lookup/NewLookup.jsx index 0721e4e4d1..0f3342e5b5 100644 --- a/awx/ui_next/src/components/Lookup/NewLookup.jsx +++ b/awx/ui_next/src/components/Lookup/NewLookup.jsx @@ -21,7 +21,6 @@ import { t } from '@lingui/macro'; import styled from 'styled-components'; import reducer, { initReducer } from './shared/reducer'; -import SelectList from './shared/SelectList'; import { ChipGroup, Chip } from '../Chip'; import { QSConfig } from '@types'; @@ -51,20 +50,28 @@ const ChipHolder = styled.div` function Lookup(props) { const { id, - items, - count, + // items, + // count, header, - name, + // name, onChange, onBlur, - columns, + // columns, value, multiple, required, qsConfig, + renderItemChip, + renderSelectList, + history, i18n, } = props; - const [state, dispatch] = useReducer(reducer, props, initReducer); + + const [state, dispatch] = useReducer( + reducer, + { value, multiple, required }, + initReducer + ); useEffect(() => { dispatch({ type: 'SET_MULTIPLE', value: multiple }); @@ -74,10 +81,18 @@ function Lookup(props) { 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' }); }; @@ -89,8 +104,12 @@ function Lookup(props) { } }; - const { isModalOpen, selectedItems } = state; + const closeModal = () => { + clearQSParams(); + dispatch({ type: 'CLOSE_MODAL' }); + }; + const { isModalOpen, selectedItems } = state; const canDelete = !required || (multiple && value.length > 1); return ( @@ -105,15 +124,13 @@ function Lookup(props) { - {(multiple ? value : [value]).map(item => ( - removeItem(item)} - isReadOnly={!canDelete} - > - {item.name} - - ))} + {(multiple ? value : [value]).map(item => + renderItemChip({ + item, + removeItem, + canDelete, + }) + )} @@ -121,7 +138,7 @@ function Lookup(props) { className="awx-c-modal" title={i18n._(t`Select ${header || i18n._(t`Items`)}`)} isOpen={isModalOpen} - onClose={() => dispatch({ type: 'TOGGLE_MODAL' })} + onClose={closeModal} actions={[ , - , ]} > - + {renderSelectList({ + state, + dispatch, + canDelete, + })} ); @@ -165,27 +171,38 @@ const Item = shape({ Lookup.propTypes = { id: string, - items: arrayOf(shape({})).isRequired, - count: number.isRequired, + // items: arrayOf(shape({})).isRequired, + // count: number.isRequired, // TODO: change to `header` header: string, - name: string, + // name: 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, - name: null, + // name: null, value: null, multiple: false, required: false, onBlur: () => {}, + renderItemChip: ({ item, removeItem, canDelete }) => ( + removeItem(item)} + isReadOnly={!canDelete} + > + {item.name} + + ), }; export { Lookup as _Lookup }; diff --git a/awx/ui_next/src/components/Lookup/shared/SelectList.jsx b/awx/ui_next/src/components/Lookup/shared/SelectList.jsx index 96db387c72..128d9371d8 100644 --- a/awx/ui_next/src/components/Lookup/shared/SelectList.jsx +++ b/awx/ui_next/src/components/Lookup/shared/SelectList.jsx @@ -26,7 +26,8 @@ function SelectList({ name, qsConfig, readOnly, - dispatch, + selectItem, + deselectItem, i18n, }) { return ( @@ -36,7 +37,7 @@ function SelectList({ label={i18n._(t`Selected`)} selected={value} showOverflowAfter={5} - onRemove={item => dispatch({ type: 'DESELECT_ITEM', item })} + onRemove={item => deselectItem(item)} isReadOnly={readOnly} /> )} @@ -53,8 +54,8 @@ function SelectList({ name={multiple ? item.name : name} label={item.name} isSelected={value.some(i => i.id === item.id)} - onSelect={() => dispatch({ type: 'SELECT_ITEM', item })} - onDeselect={() => dispatch({ type: 'DESELECT_ITEM', item })} + onSelect={() => selectItem(item)} + onDeselect={() => deselectItem(item)} isRadio={!multiple} /> )} @@ -75,7 +76,8 @@ SelectList.propTypes = { columns: arrayOf(shape({})).isRequired, multiple: bool, qsConfig: QSConfig.isRequired, - dispatch: func.isRequired, + selectItem: func.isRequired, + deselectItem: func.isRequired, }; SelectList.defaultProps = { multiple: false, diff --git a/awx/ui_next/src/components/Lookup/shared/reducer.js b/awx/ui_next/src/components/Lookup/shared/reducer.js index 2e2c88f096..9f7f6b37e0 100644 --- a/awx/ui_next/src/components/Lookup/shared/reducer.js +++ b/awx/ui_next/src/components/Lookup/shared/reducer.js @@ -1,3 +1,5 @@ +// import { useReducer, useEffect } from 'react'; + export default function reducer(state, action) { // console.log(action, state); switch (action.type) { @@ -56,33 +58,13 @@ function toggleModal(state) { } function closeModal(state) { - // TODO clear QSParams & push history state? - // state.clearQSParams(); return { ...state, isModalOpen: false, }; } -// clearQSParams() { -// const { qsConfig, history } = this.props; -// 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('&')}`); -// } -export function initReducer({ - id, - items, - count, - header, - name, - onChange, - value, - multiple = false, - required = false, - qsConfig, -}) { +export function initReducer({ value, multiple = false, required = false }) { assertCorrectValueType(value, multiple); let selectedItems = []; if (value) { @@ -94,7 +76,6 @@ export function initReducer({ multiple, isModalOpen: false, required, - onChange, }; } @@ -108,3 +89,18 @@ function assertCorrectValueType(value, multiple) { throw new Error('Lookup value must be an array if `multiple` is set'); } } +// +// export function useLookup(config) { +// const { value, multiple, required, onChange, history } = config; +// const [state, dispatch] = useReducer( +// config.reducer || reducer, +// { +// value, +// multiple, +// required, +// }, +// config.initReducer || initReducer +// ); +// +// return [state, dispatch]; +// }