From 639b297027452a55580e15ec7aab72e2ac848134 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 25 Nov 2019 14:22:34 -0800 Subject: [PATCH] fix credential chips in SelectedList, MultiCredential cleanup --- .../AnsibleSelect/AnsibleSelect.jsx | 19 +- .../src/components/Lookup/CategoryLookup.jsx | 331 ------------------ .../Lookup/MultiCredentialsLookup.jsx | 90 ++--- .../src/components/Lookup/NewLookup.jsx | 4 - awx/ui_next/src/components/Lookup/index.js | 1 - .../components/Lookup/shared/SelectList.jsx | 4 + .../components/SelectedList/SelectedList.jsx | 48 ++- 7 files changed, 69 insertions(+), 428 deletions(-) delete mode 100644 awx/ui_next/src/components/Lookup/CategoryLookup.jsx diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx index 1de791ab58..6c87032341 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -36,12 +36,12 @@ class AnsibleSelect extends React.Component { aria-label={i18n._(t`Select Input`)} isValid={isValid} > - {data.map(datum => ( + {data.map(option => ( ))} @@ -49,6 +49,13 @@ class AnsibleSelect extends React.Component { } } +const Option = shape({ + id: oneOfType([string, number]).isRequired, + value: oneOfType([string, number]).isRequired, + label: string.isRequired, + isDisabled: bool, +}); + AnsibleSelect.defaultProps = { data: [], isValid: true, @@ -56,7 +63,7 @@ AnsibleSelect.defaultProps = { }; AnsibleSelect.propTypes = { - data: arrayOf(shape()), + data: arrayOf(Option), id: string.isRequired, isValid: bool, onBlur: func, diff --git a/awx/ui_next/src/components/Lookup/CategoryLookup.jsx b/awx/ui_next/src/components/Lookup/CategoryLookup.jsx deleted file mode 100644 index 90009079bb..0000000000 --- a/awx/ui_next/src/components/Lookup/CategoryLookup.jsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { Fragment } 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, - ToolbarItem, -} from '@patternfly/react-core'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import styled from 'styled-components'; - -import AnsibleSelect from '../AnsibleSelect'; -import PaginatedDataList from '../PaginatedDataList'; -import VerticalSeperator from '../VerticalSeparator'; -import DataListToolbar from '../DataListToolbar'; -import CheckboxListItem from '../CheckboxListItem'; -import SelectedList from '../SelectedList'; -import { ChipGroup, CredentialChip } 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; -`; - -class CategoryLookup extends React.Component { - constructor(props) { - super(props); - - // this.assertCorrectValueType(); - let selectedItems = []; - if (props.value) { - selectedItems = props.multiple ? [...props.value] : [props.value]; - } - this.state = { - isModalOpen: false, - selectedItems, - error: null, - }; - 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); - } - - // assertCorrectValueType() { - // const { multiple, value, selectCategoryOptions } = this.props; - // if (selectCategoryOptions) { - // return; - // } - // if (!multiple && Array.isArray(value)) { - // throw new Error( - // 'CategoryLookup value must not be an array unless `multiple` is set' - // ); - // } - // if (multiple && !Array.isArray(value)) { - // throw new Error( - // 'CategoryLookup value must be an array if `multiple` is set' - // ); - // } - // } - - 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), - }); - } - - 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: clean up - handleModalToggle() { - const { isModalOpen } = this.state; - const { value, multiple, selectCategory } = 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(); - if (selectCategory) { - selectCategory(null, 'Machine'); - } - } - this.setState(prevState => ({ - isModalOpen: !prevState.isModalOpen, - })); - } - - removeItemAndSave(row) { - const { value, onChange, multiple } = this.props; - if (multiple) { - onChange(value.filter(item => item.id !== row.id)); - } else if (value.id === row.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 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, error } = this.state; - const { - id, - items, - count, - lookupHeader, - value, - columns, - multiple, - name, - onBlur, - qsConfig, - required, - selectCategory, - selectCategoryOptions, - selectedCategory, - i18n, - } = this.props; - const header = lookupHeader || i18n._(t`Items`); - const canDelete = !required || (multiple && value.length > 1); - return ( - - - - - - - - {(multiple ? value : [value]).map(chip => ( - this.removeItemAndSave(chip)} - isReadOnly={!canDelete} - credential={chip} - /> - ))} - - - - - {i18n._(t`Select`)} - , - , - ]} - > - {selectCategoryOptions && selectCategoryOptions.length > 0 && ( - - Selected Category - - - - )} - {selectedItems.length > 0 && ( - 0 - } - /> - )} - ( - i.id === item.id)} - onSelect={() => this.addItem(item)} - isRadio={ - !multiple || - (selectCategoryOptions && - selectCategoryOptions.length && - selectedCategory.value !== 'Vault') - } - /> - )} - renderToolbar={props => } - showPageSizeOptions={false} - /> - {error ?
error: {error.message}
: ''} -
-
- ); - } -} - -const Item = shape({ - id: number.isRequired, -}); - -CategoryLookup.propTypes = { - id: string, - items: arrayOf(shape({})).isRequired, - // TODO: change to `header` - lookupHeader: string, - name: string, - onChange: func.isRequired, - value: oneOfType([Item, arrayOf(Item)]), - multiple: bool, - required: bool, - qsConfig: QSConfig.isRequired, - selectCategory: func.isRequired, - selectCategoryOptions: oneOfType(shape({})).isRequired, - selectedCategory: shape({}).isRequired, -}; - -CategoryLookup.defaultProps = { - id: 'lookup-search', - lookupHeader: null, - name: null, - value: null, - multiple: false, - required: false, -}; - -export { CategoryLookup as _CategoryLookup }; -export default withI18n()(withRouter(CategoryLookup)); diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 7578205923..20d5180e7c 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect } from 'react'; import { withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; @@ -12,7 +12,6 @@ import VerticalSeperator from '@components/VerticalSeparator'; import { getQSConfig, parseQueryString } from '@util/qs'; import Lookup from './NewLookup'; import SelectList from './shared/SelectList'; -import multiCredentialReducer from './shared/multiCredentialReducer'; const QS_CONFIG = getQSConfig('credentials', { page: 1, @@ -20,50 +19,10 @@ const QS_CONFIG = getQSConfig('credentials', { order_by: 'name', }); -// TODO: move into reducer -function toggleCredentialSelection(credentialsToUpdate, newCredential) { - let newCredentialsList; - const isSelectedCredentialInState = - credentialsToUpdate.filter(cred => cred.id === newCredential.id).length > 0; - - if (isSelectedCredentialInState) { - newCredentialsList = credentialsToUpdate.filter( - cred => cred.id !== newCredential.id - ); - } else { - newCredentialsList = credentialsToUpdate.filter( - credential => - credential.kind === 'vault' || credential.kind !== newCredential.kind - ); - newCredentialsList = [...newCredentialsList, newCredential]; - } - return newCredentialsList; -} - 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; + return data.results.filter(type => acceptableTypes.includes(type.kind)); } async function loadCredentials(params, selectedCredentialTypeId) { @@ -78,13 +37,16 @@ function MultiCredentialsLookup(props) { const [selectedType, setSelectedType] = useState(null); const [credentials, setCredentials] = useState([]); const [credentialsCount, setCredentialsCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { (async () => { try { const types = await loadCredentialTypes(); setCredentialTypes(types); - setSelectedType(types[0]); + setSelectedType( + types.find(type => type.name === 'Machine') || types[0] + ); } catch (err) { onError(err); } @@ -98,10 +60,12 @@ 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) { @@ -111,29 +75,29 @@ function MultiCredentialsLookup(props) { }, [selectedType]); const isMultiple = selectedType && selectedType.value === 'Vault'; + const renderChip = ({ item, removeItem, canDelete }) => ( + removeItem(item)} + isReadOnly={!canDelete} + credential={item} + /> + ); + return ( {tooltip && } ( - removeItem(item)} - isReadOnly={!canDelete} - credential={item} - /> - )} + renderItemChip={renderChip} renderSelectList={({ state, dispatch, canDelete }) => { return ( - <> + {credentialTypes && credentialTypes.length > 0 && (
{i18n._(t`Selected Category`)}
@@ -142,11 +106,16 @@ function MultiCredentialsLookup(props) { css="flex: 1 1 75%;" id="multiCredentialsLookUp-select" label={i18n._(t`Selected Category`)} - data={credentialTypes} - value={selectedType && selectedType.label} - onChange={(e, label) => { + data={credentialTypes.map(type => ({ + id: type.id, + value: type.id, + label: type.name, + isDisabled: false, + }))} + value={selectedType && selectedType.id} + onChange={(e, id) => { setSelectedType( - credentialTypes.find(o => o.label === label) + credentialTypes.find(o => o.id === parseInt(id, 10)) ); }} /> @@ -183,8 +152,9 @@ function MultiCredentialsLookup(props) { }); }} deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} + renderItemChip={renderChip} /> - +
); }} /> diff --git a/awx/ui_next/src/components/Lookup/NewLookup.jsx b/awx/ui_next/src/components/Lookup/NewLookup.jsx index c0e8cfa943..8c93b18d87 100644 --- a/awx/ui_next/src/components/Lookup/NewLookup.jsx +++ b/awx/ui_next/src/components/Lookup/NewLookup.jsx @@ -50,13 +50,9 @@ const ChipHolder = styled.div` function Lookup(props) { const { id, - // items, - // count, header, - // name, onChange, onBlur, - // columns, value, multiple, required, diff --git a/awx/ui_next/src/components/Lookup/index.js b/awx/ui_next/src/components/Lookup/index.js index 5e2959c00e..cde48e2bcd 100644 --- a/awx/ui_next/src/components/Lookup/index.js +++ b/awx/ui_next/src/components/Lookup/index.js @@ -1,5 +1,4 @@ export { default } from './Lookup'; -export { default as CategoryLookup } from './CategoryLookup'; export { default as InstanceGroupsLookup } from './InstanceGroupsLookup'; export { default as InventoryLookup } from './InventoryLookup'; export { default as ProjectLookup } from './ProjectLookup'; diff --git a/awx/ui_next/src/components/Lookup/shared/SelectList.jsx b/awx/ui_next/src/components/Lookup/shared/SelectList.jsx index 128d9371d8..b9a2ec1f84 100644 --- a/awx/ui_next/src/components/Lookup/shared/SelectList.jsx +++ b/awx/ui_next/src/components/Lookup/shared/SelectList.jsx @@ -28,6 +28,7 @@ function SelectList({ readOnly, selectItem, deselectItem, + renderItemChip, i18n, }) { return ( @@ -39,6 +40,7 @@ function SelectList({ showOverflowAfter={5} onRemove={item => deselectItem(item)} isReadOnly={readOnly} + renderItemChip={renderItemChip} /> )} ( - onRemove(item)} - credential={item} - > - {item[displayKey]} - - )) - : selected.map(item => ( - onRemove(item)} - > - {item[displayKey]} - - )); + + const renderChip = + renderItemChip || + (({ item, removeItem }) => ( + + {item[displayKey]} + + )); + return ( {label} - {chips} + + {selected.map(item => + renderChip({ + item, + removeItem: () => onRemove(item), + canDelete: !isReadOnly, + }) + )} + ); @@ -67,7 +63,7 @@ SelectedList.propTypes = { onRemove: PropTypes.func, selected: PropTypes.arrayOf(PropTypes.object).isRequired, isReadOnly: PropTypes.bool, - isCredentialList: PropTypes.bool, + renderItemChip: PropTypes.func, }; SelectedList.defaultProps = { @@ -75,7 +71,7 @@ SelectedList.defaultProps = { label: 'Selected', onRemove: () => null, isReadOnly: false, - isCredentialList: false, + renderItemChip: null, }; export default SelectedList;