(props: MultiComboboxPro
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const index = virtualRow.index;
- const item = items[index];
+ const item = options[index];
const itemProps = getItemProps({ item, index });
const isSelected = isOptionSelected(item);
const id = 'multicombobox-option-' + item.value.toString();
const isAll = item.value === ALL_OPTION_VALUE;
+
+ // TODO: fix bug where if the search filtered items list is the
+ // same length, but different, than the selected items (ask tobias)
const allItemsSelected =
- items[0]?.value === ALL_OPTION_VALUE && selectedItems.length === items.length - 1;
+ options[0]?.value === ALL_OPTION_VALUE && selectedItems.length === options.length - 1;
return (
- (props: MultiComboboxPro
label={
isAll
? (item.label ?? item.value.toString()) +
- (isAll && inputValue !== '' ? ` (${items.length - 1})` : '')
+ (isAll && inputValue !== '' ? ` (${options.length - 1})` : '')
: (item.label ?? item.value.toString())
}
description={item?.description}
@@ -332,7 +343,7 @@ export const MultiCombobox = (props: MultiComboboxPro
);
})}
- {items.length === 0 && }
+ {options.length === 0 && }
)}
@@ -375,7 +386,3 @@ function isComboboxOptions(
): value is Array> {
return typeof value[0] === 'object';
}
-
-function getComboboxOptionsValues(optionArray: Array>) {
- return optionArray.map((option) => option.value);
-}
diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts
new file mode 100644
index 00000000000..504f3584e1d
--- /dev/null
+++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts
@@ -0,0 +1,82 @@
+import { debounce } from 'lodash';
+import { useState, useCallback, useMemo } from 'react';
+
+import { itemFilter } from './filter';
+import { ComboboxOption } from './types';
+import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall';
+
+type AsyncOptions =
+ | Array>
+ | ((inputValue: string) => Promise>>);
+
+const asyncNoop = () => Promise.resolve([]);
+
+/**
+ * Abstracts away sync/async options for MultiCombobox (and later Combobox).
+ * It also filters options based on the user's input.
+ *
+ * Returns:
+ * - options either filtered by user's input, or from async options fn
+ * - function to call when user types (to filter, or call async fn)
+ * - loading and error states
+ */
+export function useOptions(rawOptions: AsyncOptions) {
+ const isAsync = typeof rawOptions === 'function';
+
+ const loadOptions = useLatestAsyncCall(isAsync ? rawOptions : asyncNoop);
+
+ const debouncedLoadOptions = useMemo(
+ () =>
+ debounce((searchTerm: string) => {
+ return loadOptions(searchTerm)
+ .then((options) => {
+ setAsyncOptions(options);
+ setAsyncLoading(false);
+ setAsyncError(false);
+ })
+ .catch((error) => {
+ if (!(error instanceof StaleResultError)) {
+ setAsyncError(true);
+ setAsyncLoading(false);
+
+ if (error) {
+ console.error('Error loading async options for Combobox', error);
+ }
+ }
+ });
+ }, 200),
+ [loadOptions]
+ );
+
+ const [asyncOptions, setAsyncOptions] = useState>>([]);
+ const [asyncLoading, setAsyncLoading] = useState(false);
+ const [asyncError, setAsyncError] = useState(false);
+
+ // This hook keeps its own inputValue state (rather than accepting it as an arg) because it needs to be
+ // told it for async options loading anyway.
+ const [userTypedSearch, setUserTypedSearch] = useState('');
+
+ const updateOptions = useCallback(
+ (inputValue: string) => {
+ if (!isAsync) {
+ setUserTypedSearch(inputValue);
+ return;
+ }
+
+ setAsyncLoading(true);
+
+ debouncedLoadOptions(inputValue);
+ },
+ [debouncedLoadOptions, isAsync]
+ );
+
+ const finalOptions = useMemo(() => {
+ if (isAsync) {
+ return asyncOptions;
+ } else {
+ return rawOptions.filter(itemFilter(userTypedSearch));
+ }
+ }, [rawOptions, asyncOptions, isAsync, userTypedSearch]);
+
+ return { options: finalOptions, updateOptions, asyncLoading, asyncError };
+}