Skip to content

Commit db79be5

Browse files
committed
Make event metadata load faster by fetching series options asynchronously
If you have many series in your Opencast, any modal that displays event metadata can take several seconds to load. This patch aims at pushing the loading time back into millisecond terrritory no matter how many series there are. The problem is that when fetching event metadata from the backend, the backend also fetch all series options, which can take too long. So we change the backend to not do that, and change the frontend to fetch the series options separately. ideally we could also do this for other metadata fields with options (notably contributors and presenters), but the endpoint lacks the necessary filter capabilities.
1 parent 57614f1 commit db79be5

4 files changed

Lines changed: 136 additions & 43 deletions

File tree

src/components/shared/DropDown.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const DropDown = <T, >({
4545
ref?: React.RefObject<SelectInstance<any, boolean, GroupBase<any>> | null>
4646
value: T
4747
text: string,
48-
options: DropDownOption[],
48+
options?: DropDownOption[],
4949
required: boolean,
5050
handleChange: (option: {value: T, label: string} | null) => void
5151
placeholder: string
@@ -66,7 +66,7 @@ const DropDown = <T, >({
6666
optionPaddingTop?: number,
6767
optionLineHeight?: string
6868
},
69-
fetchOptions?: () => { label: string, value: string}[]
69+
fetchOptions?: (inputValue: string) => Promise<{ label: string, value: string }[]>
7070
}) => {
7171
const { t } = useTranslation();
7272

@@ -157,14 +157,29 @@ const DropDown = <T, >({
157157
) : null;
158158
};
159159

160+
const filterOptions = (inputValue: string) => {
161+
if (options) {
162+
return options.filter(option =>
163+
option.label.toLowerCase().includes(inputValue.toLowerCase()),
164+
);
165+
}
166+
return [];
167+
};
168+
169+
const loadOptionsAsync = (inputValue: string, callback: (options: DropDownOption[]) => void) => {
170+
setTimeout(async () => {
171+
callback(formatOptions(
172+
fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue),
173+
required,
174+
));
175+
}, 1000);
176+
};
177+
160178
const loadOptions = (
161179
inputValue: string,
162180
callback: (options: DropDownOption[]) => void,
163181
) => {
164-
callback(formatOptions(
165-
fetchOptions ? fetchOptions() : options,
166-
required,
167-
));
182+
callback(formatOptions(filterOptions(inputValue), required));
168183
};
169184

170185

@@ -176,10 +191,14 @@ const DropDown = <T, >({
176191
autoFocus: autoFocus,
177192
isSearchable: true,
178193
value: { value: value, label: text === "" ? placeholder : text },
179-
options: formatOptions(
180-
options,
181-
required,
182-
),
194+
defaultOptions: options
195+
? formatOptions(
196+
options,
197+
required,
198+
)
199+
: true,
200+
cacheOptions: true,
201+
loadOptions: fetchOptions ? loadOptionsAsync : loadOptions,
183202
placeholder: placeholder,
184203
onChange: element => handleChange(element as {value: T, label: string}),
185204
menuIsOpen: menuIsOpen,
@@ -191,31 +210,18 @@ const DropDown = <T, >({
191210

192211
// @ts-expect-error: React-Select typing does not account for the typing of option it itself requires
193212
components: { MenuList },
194-
filterOption: createFilter({ ignoreAccents: false }), // To improve performance on filtering
195213
};
196214

197215
return creatable ? (
198216
<AsyncCreatableSelect
199217
ref={selectRef}
200218
{...commonProps}
201-
cacheOptions
202-
defaultOptions={formatOptions(
203-
options,
204-
required,
205-
)}
206-
loadOptions={loadOptions}
207219
/>
208220
) : (
209221
<AsyncSelect
210222
ref={selectRef}
211223
{...commonProps}
212224
noOptionsMessage={() => t("SELECT_NO_MATCHING_RESULTS")}
213-
cacheOptions
214-
defaultOptions={formatOptions(
215-
options,
216-
required,
217-
)}
218-
loadOptions={loadOptions}
219225
/>
220226
);
221227
};

src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,11 +543,6 @@ export const AccessPolicyTable = <T extends AccessPolicyTabFormikProps>({
543543
? formatAclRolesForDropdown(rolesFilteredbyPolicies)
544544
: []
545545
}
546-
fetchOptions={() =>
547-
roles.length > 0
548-
? formatAclRolesForDropdown(rolesFilteredbyPolicies)
549-
: []
550-
}
551546
required={true}
552547
creatable={true}
553548
handleChange={element => {

src/components/shared/wizard/RenderField.tsx

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import React, { useRef, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import DatePicker from "react-datepicker";
44
import cn from "classnames";
5-
import { getMetadataCollectionFieldName } from "../../../utils/resourceUtils";
5+
import { getMetadataCollectionFieldName, transformListProvider } from "../../../utils/resourceUtils";
66
import { getCurrentLanguageInformation } from "../../../utils/utils";
77
import DropDown from "../DropDown";
88
import { parseISO } from "date-fns";
99
import { FieldProps } from "formik";
1010
import { MetadataField } from "../../../slices/eventSlice";
1111
import { GroupBase, SelectInstance } from "react-select";
1212
import TextareaAutosize from "react-textarea-autosize";
13+
import axios from "axios";
1314

1415
/**
1516
* This component renders an editable field for single values depending on the type of the corresponding metadata
@@ -65,7 +66,7 @@ const RenderField = ({
6566
)}
6667
{metadataField.type === "text" &&
6768
!!metadataField.collection &&
68-
metadataField.collection.length > 0 && (
69+
(
6970
<EditableSingleSelect
7071
metadataField={metadataField}
7172
field={field}
@@ -91,7 +92,7 @@ const RenderField = ({
9192
)}
9293
{metadataField.type === "text" &&
9394
!(
94-
!!metadataField.collection && metadataField.collection.length !== 0
95+
metadataField.collection
9596
) && (
9697
<EditableSingleValue
9798
field={field}
@@ -195,16 +196,7 @@ const EditableDateValue = ({
195196
};
196197

197198
// renders editable field for selecting value via dropdown
198-
const EditableSingleSelect = ({
199-
field,
200-
metadataField,
201-
text,
202-
form: { setFieldValue },
203-
isFirstField,
204-
focused,
205-
setFocused,
206-
ref,
207-
}: {
199+
type EditableSingleSelectProps = ({
208200
field: FieldProps["field"]
209201
metadataField: MetadataField
210202
text: string
@@ -213,9 +205,25 @@ const EditableSingleSelect = ({
213205
focused: boolean,
214206
setFocused: (open: boolean) => void
215207
ref: React.RefObject<SelectInstance<any, boolean, GroupBase<any>>>
216-
}) => {
208+
})
209+
const EditableSingleSelect = (props: EditableSingleSelectProps) => {
217210
const { t } = useTranslation();
218211

212+
const {
213+
field,
214+
metadataField,
215+
text,
216+
form: { setFieldValue },
217+
isFirstField,
218+
focused,
219+
setFocused,
220+
ref,
221+
} = props;
222+
223+
if (metadataField.id === "isPartOf") {
224+
return <EditableSingleSelectSeries {...props} />;
225+
}
226+
219227
return (
220228
<DropDown
221229
ref={ref}
@@ -321,4 +329,67 @@ const EditableSingleValueTime = ({
321329
);
322330
};
323331

332+
/**
333+
* Special case for series. Uses an async selector to fetch options.
334+
*
335+
* Ideally we could generalize this for all metadata fields with listproviders,
336+
* but other listproviders do not offer the required filtering capabilities.
337+
*/
338+
const EditableSingleSelectSeries = ({
339+
field,
340+
metadataField,
341+
text,
342+
form: { setFieldValue },
343+
isFirstField,
344+
focused,
345+
setFocused,
346+
ref,
347+
}: EditableSingleSelectProps) => {
348+
const { t } = useTranslation();
349+
350+
const [label, setLabel] = useState("");
351+
352+
useEffect(() => {
353+
// The metadata catalog only contains the field value, so we need to fetch the label ourselves
354+
const fetchLabelById = async () => {
355+
if (field.value) {
356+
const res = await axios.get<{ [key: string]: string }>(`/admin-ng/resources/SERIES.WRITE_ONLY.json?limit=1&filter=textFilter:${field.value}`);
357+
const data = res.data;
358+
const transformedData = transformListProvider(data);
359+
if (transformedData.length > 0) {
360+
setLabel(transformedData[0].label);
361+
}
362+
}
363+
};
364+
fetchLabelById();
365+
}, [field.value]);
366+
367+
// Fetch collection
368+
const fetchOptions = async (inputValue: string) => {
369+
const res = await axios.get<{ [key: string]: string }>(`/admin-ng/resources/SERIES.WRITE_ONLY.json?filter=textFilter:*${inputValue}*`);
370+
const data = res.data;
371+
return transformListProvider(data);
372+
};
373+
374+
return (
375+
<DropDown
376+
ref={ref}
377+
value={field.value as string}
378+
text={label}
379+
fetchOptions={fetchOptions}
380+
required={metadataField.required}
381+
handleChange={element => element && setFieldValue(field.name, element.value)}
382+
placeholder={focused
383+
? `-- ${t("SELECT_NO_OPTION_SELECTED")} --`
384+
: `${t("SELECT_NO_OPTION_SELECTED")}`
385+
}
386+
customCSS={{ isMetadataStyle: focused ? false : true }}
387+
handleMenuIsOpen={(open: boolean) => setFocused(open)}
388+
openMenuOnFocus
389+
autoFocus={isFirstField}
390+
skipTranslate={!metadataField.translatable}
391+
/>
392+
);
393+
};
394+
324395
export default RenderField;

src/utils/resourceUtils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ export const transformMetadataFields = (metadata: MetadataField[]) => {
177177
return metadata;
178178
};
179179

180+
export const transformListProvider = (collection: { [key: string]: string }) => {
181+
return Object.entries(collection)
182+
.map(([key, value]) => {
183+
if (isJson(value)) {
184+
// TODO: Handle JSON parsing errors
185+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
186+
const collectionParsed: { [key: string]: string } = JSON.parse(value);
187+
return {
188+
label: collectionParsed.label || value,
189+
value: key,
190+
...collectionParsed,
191+
};
192+
} else {
193+
return {
194+
label: value,
195+
value: key,
196+
};
197+
}
198+
});
199+
};
200+
180201
// transform metadata catalog for update via post request
181202
export const transformMetadataForUpdate = (catalog: MetadataCatalog, values: { [key: string]: MetadataCatalog["fields"][0]["value"] }) => {
182203
const fields: MetadataCatalog["fields"] = [];

0 commit comments

Comments
 (0)