Disable field inputs while fetching data

In the JT form, disable the Lookup and Select box fields for any
fields that need to fetch data, until data fetching is complete
This commit is contained in:
Keith Grant
2020-05-18 15:39:43 -07:00
parent 9d3b19341d
commit af118fec99
7 changed files with 130 additions and 83 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { arrayOf, string, func, object, bool } from 'prop-types'; import { arrayOf, string, func, object, bool } from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
@@ -8,6 +8,7 @@ import { InstanceGroupsAPI } from '../../api';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
import { FieldTooltip } from '../FormField'; import { FieldTooltip } from '../FormField';
import OptionsList from '../OptionsList'; import OptionsList from '../OptionsList';
import useRequest from '../../util/useRequest';
import Lookup from './Lookup'; import Lookup from './Lookup';
import LookupErrorMessage from './shared/LookupErrorMessage'; import LookupErrorMessage from './shared/LookupErrorMessage';
@@ -27,22 +28,27 @@ function InstanceGroupsLookup(props) {
history, history,
i18n, i18n,
} = props; } = props;
const [instanceGroups, setInstanceGroups] = useState([]);
const [count, setCount] = useState(0); const {
const [error, setError] = useState(null); result: { instanceGroups, count },
request: fetchInstanceGroups,
error,
isLoading,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
const { data } = await InstanceGroupsAPI.read(params);
return {
instanceGroups: data.results,
count: data.count,
};
}, [history.location]),
{ instanceGroups: [], count: 0 }
);
useEffect(() => { useEffect(() => {
(async () => { fetchInstanceGroups();
const params = parseQueryString(QS_CONFIG, history.location.search); }, [fetchInstanceGroups]);
try {
const { data } = await InstanceGroupsAPI.read(params);
setInstanceGroups(data.results);
setCount(data.count);
} catch (err) {
setError(err);
}
})();
}, [history.location]);
return ( return (
<FormGroup <FormGroup
@@ -59,6 +65,7 @@ function InstanceGroupsLookup(props) {
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
multiple multiple
required={required} required={required}
isLoading={isLoading}
renderOptionsList={({ state, dispatch, canDelete }) => ( renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList <OptionsList
value={state.selectedItems} value={state.selectedItems}

View File

@@ -19,22 +19,20 @@ const QS_CONFIG = getQSConfig('inventory', {
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) { function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
const { const {
result: { count, inventories }, result: { inventories, count },
error,
request: fetchInventories, request: fetchInventories,
error,
isLoading,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search); const params = parseQueryString(QS_CONFIG, history.location.search);
const { data } = await InventoriesAPI.read(params); const { data } = await InventoriesAPI.read(params);
return { return {
count: data.count,
inventories: data.results, inventories: data.results,
count: data.count,
}; };
}, [history.location.search]), }, [history.location]),
{ { inventories: [], count: 0 }
count: 0,
inventories: [],
}
); );
useEffect(() => { useEffect(() => {
@@ -50,6 +48,7 @@ function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
onChange={onChange} onChange={onChange}
onBlur={onBlur} onBlur={onBlur}
required={required} required={required}
isLoading={isLoading}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => ( renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList <OptionsList

View File

@@ -56,6 +56,7 @@ function Lookup(props) {
header, header,
onChange, onChange,
onBlur, onBlur,
isLoading,
value, value,
multiple, multiple,
required, required,
@@ -124,6 +125,7 @@ function Lookup(props) {
id={id} id={id}
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })} onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
variant={ButtonVariant.tertiary} variant={ButtonVariant.tertiary}
isDisabled={isLoading}
> >
<SearchIcon /> <SearchIcon />
</SearchButton> </SearchButton>

View File

@@ -1,5 +1,5 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React, { Fragment, useState, useEffect } from 'react'; import React, { Fragment, useState, useCallback, useEffect } from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
@@ -9,6 +9,7 @@ import { CredentialsAPI, CredentialTypesAPI } from '../../api';
import AnsibleSelect from '../AnsibleSelect'; import AnsibleSelect from '../AnsibleSelect';
import CredentialChip from '../CredentialChip'; import CredentialChip from '../CredentialChip';
import OptionsList from '../OptionsList'; import OptionsList from '../OptionsList';
import useRequest from '../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../util/qs'; import { getQSConfig, parseQueryString } from '../../util/qs';
import Lookup from './Lookup'; import Lookup from './Lookup';
@@ -26,42 +27,62 @@ async function loadCredentials(params, selectedCredentialTypeId) {
function MultiCredentialsLookup(props) { function MultiCredentialsLookup(props) {
const { value, onChange, onError, history, i18n } = props; const { value, onChange, onError, history, i18n } = props;
const [credentialTypes, setCredentialTypes] = useState([]);
const [selectedType, setSelectedType] = useState(null); const [selectedType, setSelectedType] = useState(null);
const [credentials, setCredentials] = useState([]);
const [credentialsCount, setCredentialsCount] = useState(0); const {
result: credentialTypes,
request: fetchTypes,
error: typesError,
isLoading: isTypesLoading,
} = useRequest(
useCallback(async () => {
const types = await CredentialTypesAPI.loadAllTypes();
const match = types.find(type => type.kind === 'ssh') || types[0];
setSelectedType(match);
return types;
}, []),
[]
);
useEffect(() => { useEffect(() => {
(async () => { fetchTypes();
try { }, [fetchTypes]);
const types = await CredentialTypesAPI.loadAllTypes();
setCredentialTypes(types);
const match = types.find(type => type.kind === 'ssh') || types[0];
setSelectedType(match);
} catch (err) {
onError(err);
}
})();
}, [onError]);
useEffect(() => { const {
(async () => { result: { credentials, credentialsCount },
request: fetchCredentials,
error: credentialsError,
isLoading: isCredentialsLoading,
} = useRequest(
useCallback(async () => {
if (!selectedType) { if (!selectedType) {
return; return {
credentials: [],
count: 0,
};
} }
try { const params = parseQueryString(QS_CONFIG, history.location.search);
const params = parseQueryString(QS_CONFIG, history.location.search); const { results, count } = await loadCredentials(params, selectedType.id);
const { results, count } = await loadCredentials( return {
params, credentials: results,
selectedType.id credentialsCount: count,
); };
setCredentials(results); }, [selectedType, history.location]),
setCredentialsCount(count); {
} catch (err) { credentials: [],
onError(err); credentialsCount: 0,
} }
})(); );
}, [selectedType, history.location.search, onError]);
useEffect(() => {
fetchCredentials();
}, [fetchCredentials]);
useEffect(() => {
if (typesError || credentialsError) {
onError(typesError || credentialsError);
}
}, [typesError, credentialsError, onError]);
const renderChip = ({ item, removeItem, canDelete }) => ( const renderChip = ({ item, removeItem, canDelete }) => (
<CredentialChip <CredentialChip
@@ -82,6 +103,7 @@ function MultiCredentialsLookup(props) {
multiple multiple
onChange={onChange} onChange={onChange}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
isLoading={isTypesLoading || isCredentialsLoading}
renderItemChip={renderChip} renderItemChip={renderChip}
renderOptionsList={({ state, dispatch, canDelete }) => { renderOptionsList={({ state, dispatch, canDelete }) => {
return ( return (

View File

@@ -32,9 +32,10 @@ function ProjectLookup({
history, history,
}) { }) {
const { const {
result: { count, projects }, result: { projects, count },
error,
request: fetchProjects, request: fetchProjects,
error,
isLoading,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search); const params = parseQueryString(QS_CONFIG, history.location.search);
@@ -74,6 +75,7 @@ function ProjectLookup({
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
required={required} required={required}
isLoading={isLoading}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => ( renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList <OptionsList

View File

@@ -30,6 +30,7 @@ async function loadLabelOptions(setLabels, onError) {
} }
function LabelSelect({ value, placeholder, onChange, onError, createText }) { function LabelSelect({ value, placeholder, onChange, onError, createText }) {
const [isLoading, setIsLoading] = useState(true);
const { selections, onSelect, options, setOptions } = useSyncedSelectValue( const { selections, onSelect, options, setOptions } = useSyncedSelectValue(
value, value,
onChange onChange
@@ -41,7 +42,10 @@ function LabelSelect({ value, placeholder, onChange, onError, createText }) {
}; };
useEffect(() => { useEffect(() => {
loadLabelOptions(setOptions, onError); (async () => {
await loadLabelOptions(setOptions, onError);
setIsLoading(false);
})();
/* eslint-disable-next-line react-hooks/exhaustive-deps */ /* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []); }, []);
@@ -77,6 +81,7 @@ function LabelSelect({ value, placeholder, onChange, onError, createText }) {
} }
return label; return label;
}} }}
isDisabled={isLoading}
selections={selections} selections={selections}
isExpanded={isExpanded} isExpanded={isExpanded}
ariaLabelledBy="label-select" ariaLabelledBy="label-select"

View File

@@ -1,39 +1,48 @@
import React, { useState, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { number, string, oneOfType } from 'prop-types'; import { number, string, oneOfType } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import AnsibleSelect from '../../../components/AnsibleSelect'; import AnsibleSelect from '../../../components/AnsibleSelect';
import { ProjectsAPI } from '../../../api'; import { ProjectsAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) { function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
const [options, setOptions] = useState([]); const {
result: options,
request: fetchOptions,
isLoading,
error,
} = useRequest(
useCallback(async () => {
const { data } = await ProjectsAPI.readPlaybooks(projectId);
const opts = (data || []).map(playbook => ({
value: playbook,
key: playbook,
label: playbook,
isDisabled: false,
}));
opts.unshift({
value: '',
key: '',
label: i18n._(t`Choose a playbook`),
isDisabled: false,
});
return opts;
}, [projectId, i18n]),
[]
);
useEffect(() => { useEffect(() => {
if (!projectId) { fetchOptions();
return; }, [fetchOptions]);
}
(async () => { useEffect(() => {
try { if (error) {
const { data } = await ProjectsAPI.readPlaybooks(projectId); onError(error);
const opts = (data || []).map(playbook => ({ }
value: playbook, }, [error, onError]);
key: playbook,
label: playbook,
isDisabled: false,
}));
opts.unshift({
value: '',
key: '',
label: i18n._(t`Choose a playbook`),
isDisabled: false,
});
setOptions(opts);
} catch (contentError) {
onError(contentError);
}
})();
}, [projectId, i18n, onError]);
return ( return (
<AnsibleSelect <AnsibleSelect
id="template-playbook" id="template-playbook"
@@ -41,6 +50,7 @@ function PlaybookSelect({ projectId, isValid, field, onBlur, onError, i18n }) {
isValid={isValid} isValid={isValid}
{...field} {...field}
onBlur={onBlur} onBlur={onBlur}
isDisabled={isLoading}
/> />
); );
} }