Skip to content

Commit cfed054

Browse files
authored
chore(orchestrator): debounce fetches (#780)
Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent 2f55cbd commit cfed054

4 files changed

Lines changed: 136 additions & 111 deletions

File tree

workspaces/orchestrator/packages/app/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const app = createApp({
102102
),
103103
},
104104
themes: getThemes(),
105-
/* Hardcoded depolyment of the Orchestrator Form Widget library in our DEV-only instance.
105+
/* Hardcoded deployment of the Orchestrator Form Widget library in our DEV-only instance.
106106
In a production deployment, the plugin will be loaded dynamically. */
107107
plugins: [orchestratorFormWidgetsPlugin],
108108
});

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
16+
import React, { useCallback, useMemo, useState } from 'react';
1717
import { JsonObject } from '@backstage/types';
1818
import { Widget } from '@rjsf/utils';
1919
import { fetchApiRef, useApi } from '@backstage/core-plugin-api';
2020
import { JSONSchema7 } from 'json-schema';
21+
import { useDebounce } from 'react-use';
2122

2223
import { useWrapperFormPropsContext } from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-api';
2324
import { FormContextData } from '../types';
@@ -34,6 +35,7 @@ import {
3435
applySelectorString,
3536
} from '../utils/applySelector';
3637
import { Autocomplete, AutocompleteRenderInputParams } from '@material-ui/lab';
38+
import { DEFAULT_DEBOUNCE_LIMIT } from './constants';
3739

3840
export const ActiveTextInput: Widget<
3941
JsonObject,
@@ -43,7 +45,6 @@ export const ActiveTextInput: Widget<
4345
const fetchApi = useApi(fetchApiRef);
4446
const templateUnitEvaluator = useTemplateUnitEvaluator();
4547
const formContext = useWrapperFormPropsContext();
46-
const [_, setLoading] = useState(true);
4748
const [error, setError] = useState<string>();
4849
const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>();
4950

@@ -65,6 +66,7 @@ export const ActiveTextInput: Widget<
6566
/* This is safe retype, since proper checking of input value is done in the useRetriggerEvaluate() hook */
6667
uiProps['fetch:retrigger'] as string[],
6768
);
69+
const isValueSet = value === undefined;
6870

6971
const evaluatedFetchUrl = useEvaluateTemplate({
7072
template: fetchUrl,
@@ -86,8 +88,8 @@ export const ActiveTextInput: Widget<
8688
[onChange],
8789
);
8890

89-
useEffect(() => {
90-
const fetchDefaultData = async () => {
91+
useDebounce(
92+
() => {
9193
if (
9294
!evaluatedFetchUrl ||
9395
!evaluatedRequestInit ||
@@ -97,70 +99,74 @@ export const ActiveTextInput: Widget<
9799
// Not yet ready to fetch
98100
return;
99101
}
100-
const firstTime = value === undefined;
101-
if (!firstTime && !autocompleteSelector) {
102+
103+
if (!isValueSet && !autocompleteSelector) {
102104
// No need to fetch
103105
return;
104106
}
105107

106-
try {
107-
setLoading(true);
108-
setError(undefined);
108+
const fetchDefaultData = async () => {
109+
try {
110+
setError(undefined);
109111

110-
const response = await fetchApi.fetch(
111-
evaluatedFetchUrl,
112-
evaluatedRequestInit,
113-
);
114-
const data = (await response.json()) as JsonObject;
112+
const response = await fetchApi.fetch(
113+
evaluatedFetchUrl,
114+
evaluatedRequestInit,
115+
);
116+
const data = (await response.json()) as JsonObject;
115117

116-
// validate received response before updating
117-
if (!data) {
118-
throw new Error('Empty response received');
119-
}
120-
if (typeof data !== 'object') {
121-
throw new Error('JSON object expected');
122-
}
118+
// validate received response before updating
119+
if (!data) {
120+
throw new Error('Empty response received');
121+
}
122+
if (typeof data !== 'object') {
123+
throw new Error('JSON object expected');
124+
}
123125

124-
const selected = await applySelectorString(data, defaultValueSelector);
125-
if (autocompleteSelector) {
126-
const autocompleteValues = await applySelectorArray(
126+
const selected = await applySelectorString(
127127
data,
128-
autocompleteSelector,
128+
defaultValueSelector,
129+
);
130+
if (autocompleteSelector) {
131+
const autocompleteValues = await applySelectorArray(
132+
data,
133+
autocompleteSelector,
134+
);
135+
setAutocompleteOptions(autocompleteValues);
136+
}
137+
138+
if (isValueSet) {
139+
// loading default so do it only once
140+
handleChange(selected);
141+
}
142+
} catch (err) {
143+
// eslint-disable-next-line no-console
144+
console.error(
145+
'Error when fetching default ActiveTextInput data',
146+
props.id,
147+
evaluatedFetchUrl,
148+
err,
129149
);
130-
setAutocompleteOptions(autocompleteValues);
150+
setError(`Failed to fetch data for ${props.id} ActiveTextInput`);
131151
}
152+
};
132153

133-
if (firstTime) {
134-
// loading default so do it only once
135-
handleChange(selected);
136-
}
137-
} catch (err) {
138-
// eslint-disable-next-line no-console
139-
console.error(
140-
'Error when fetching default ActiveTextInput data',
141-
props.id,
142-
evaluatedFetchUrl,
143-
err,
144-
);
145-
setError(`Failed to fetch data for ${props.id} ActiveTextInput`);
146-
} finally {
147-
setLoading(false);
148-
}
149-
};
150-
151-
fetchDefaultData();
152-
}, [
153-
evaluatedFetchUrl,
154-
evaluatedRequestInit,
155-
autocompleteSelector,
156-
defaultValueSelector,
157-
fetchApi,
158-
props.id,
159-
value,
160-
handleChange,
161-
// no need to expand the "retrigger" array here since its identity changes only if an item changes
162-
retrigger,
163-
]);
154+
fetchDefaultData();
155+
},
156+
DEFAULT_DEBOUNCE_LIMIT,
157+
[
158+
evaluatedFetchUrl,
159+
evaluatedRequestInit,
160+
autocompleteSelector,
161+
defaultValueSelector,
162+
fetchApi,
163+
props.id,
164+
handleChange,
165+
isValueSet,
166+
// no need to expand the "retrigger" array here since its identity changes only if an item changes
167+
retrigger,
168+
],
169+
);
164170

165171
if (!fetchUrl || !defaultValueSelector) {
166172
// eslint-disable-next-line no-console

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import React, { useEffect, useMemo, useState } from 'react';
16+
import React, { useMemo, useState } from 'react';
1717
import { Widget } from '@rjsf/utils';
1818
import { JSONSchema7 } from 'json-schema';
1919
import { JsonObject } from '@backstage/types';
@@ -22,6 +22,7 @@ import {
2222
SchemaChunksResponse,
2323
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-api';
2424
import { fetchApiRef, useApi } from '@backstage/core-plugin-api';
25+
import { useDebounce } from 'react-use';
2526

2627
import { FormContextData } from '../types';
2728
import {
@@ -31,6 +32,7 @@ import {
3132
useTemplateUnitEvaluator,
3233
} from '../utils';
3334
import { ErrorText } from './ErrorText';
35+
import { DEFAULT_DEBOUNCE_LIMIT } from './constants';
3436

3537
export const SchemaUpdater: Widget<
3638
JsonObject,
@@ -41,7 +43,6 @@ export const SchemaUpdater: Widget<
4143
const templateUnitEvaluator = useTemplateUnitEvaluator();
4244

4345
const formContext = useWrapperFormPropsContext();
44-
const [_, setLoading] = useState(true);
4546
const [error, setError] = useState<string>();
4647

4748
const { updateSchema, formData } = formContext;
@@ -72,64 +73,66 @@ export const SchemaUpdater: Widget<
7273
setError,
7374
});
7475

75-
useEffect(() => {
76-
const fetchSchemaChunks = async () => {
76+
useDebounce(
77+
() => {
7778
if (!evaluatedFetchUrl || !retrigger || !evaluatedRequestInit) {
7879
return;
7980
}
8081

81-
try {
82-
setLoading(true);
83-
setError(undefined);
82+
const fetchSchemaChunks = async () => {
83+
try {
84+
setError(undefined);
8485

85-
const response = await fetchApi.fetch(
86-
evaluatedFetchUrl,
87-
evaluatedRequestInit,
88-
);
89-
const data = (await response.json()) as unknown as SchemaChunksResponse;
86+
const response = await fetchApi.fetch(
87+
evaluatedFetchUrl,
88+
evaluatedRequestInit,
89+
);
90+
const data =
91+
(await response.json()) as unknown as SchemaChunksResponse;
9092

91-
// validate received response before updating
92-
if (!data) {
93-
throw new Error('Empty response received');
94-
}
95-
if (typeof data !== 'object') {
96-
throw new Error('JSON object expected');
97-
}
98-
Object.keys(data).forEach(key => {
99-
if (!data[key].type) {
100-
throw new Error(
101-
`JSON response malformed, missing "type" field for "${key}" key`,
102-
);
93+
// validate received response before updating
94+
if (!data) {
95+
throw new Error('Empty response received');
10396
}
104-
});
105-
106-
updateSchema(data);
107-
} catch (err) {
108-
// eslint-disable-next-line no-console
109-
console.error(
110-
'Error when updating schema',
111-
props.id,
112-
evaluatedFetchUrl,
113-
err,
114-
);
115-
setError(
116-
`Failed to fetch schema update by the ${props.id} SchemaUpdater`,
117-
);
118-
} finally {
119-
setLoading(false);
120-
}
121-
};
122-
123-
fetchSchemaChunks();
124-
}, [
125-
evaluatedFetchUrl,
126-
evaluatedRequestInit,
127-
fetchApi,
128-
props.id,
129-
updateSchema,
130-
// no need to expand the "retrigger" array here since its identity changes only if an item changes
131-
retrigger,
132-
]);
97+
if (typeof data !== 'object') {
98+
throw new Error('JSON object expected');
99+
}
100+
Object.keys(data).forEach(key => {
101+
if (!data[key].type) {
102+
throw new Error(
103+
`JSON response malformed, missing "type" field for "${key}" key`,
104+
);
105+
}
106+
});
107+
108+
updateSchema(data);
109+
} catch (err) {
110+
// eslint-disable-next-line no-console
111+
console.error(
112+
'Error when updating schema',
113+
props.id,
114+
evaluatedFetchUrl,
115+
err,
116+
);
117+
setError(
118+
`Failed to fetch schema update by the ${props.id} SchemaUpdater`,
119+
);
120+
}
121+
};
122+
123+
fetchSchemaChunks();
124+
},
125+
DEFAULT_DEBOUNCE_LIMIT,
126+
[
127+
evaluatedFetchUrl,
128+
evaluatedRequestInit,
129+
fetchApi,
130+
props.id,
131+
updateSchema,
132+
// no need to expand the "retrigger" array here since its identity changes only if an item changes
133+
retrigger,
134+
],
135+
);
133136

134137
if (!fetchUrl) {
135138
// eslint-disable-next-line no-console
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
export const DEFAULT_DEBOUNCE_LIMIT = 1000;

0 commit comments

Comments
 (0)