fix: Adds a loading message when needed in the Select component (#16531)

diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index e6dd867..33f1ce6 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -300,6 +300,7 @@
 ];
 
 export const AsyncSelect = ({
+  fetchOnlyOnSearch,
   withError,
   withInitialValue,
   responseTime,
@@ -381,7 +382,9 @@
       >
         <Select
           {...rest}
+          fetchOnlyOnSearch={fetchOnlyOnSearch}
           options={withError ? fetchUserListError : fetchUserListPage}
+          placeholder={fetchOnlyOnSearch ? 'Type anything' : 'Select...'}
           value={
             withInitialValue
               ? { label: 'Valentina', value: 'Valentina' }
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 596722e..22c964a 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -137,6 +137,13 @@
   margin-top: ${({ theme }) => -theme.gridUnit}px;
 `;
 
+const StyledLoadingText = styled.span`
+  ${({ theme }) => `
+    margin-left: ${theme.gridUnit * 3}px;
+    color: ${theme.colors.grayscale.light1};
+  `}
+`;
+
 const MAX_TAG_COUNT = 4;
 const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
 const DEBOUNCE_TIMEOUT = 500;
@@ -175,7 +182,8 @@
   );
   const [selectValue, setSelectValue] = useState(value);
   const [searchedValue, setSearchedValue] = useState('');
-  const [isLoading, setLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [isTyping, setIsTyping] = useState(false);
   const [error, setError] = useState('');
   const [isDropdownVisible, setIsDropdownVisible] = useState(false);
   const [page, setPage] = useState(0);
@@ -350,9 +358,10 @@
       const cachedCount = fetchedQueries.current.get(key);
       if (cachedCount) {
         setTotalCount(cachedCount);
+        setIsTyping(false);
         return;
       }
-      setLoading(true);
+      setIsLoading(true);
       const fetchOptions = options as OptionsPagePromise;
       fetchOptions(value, page, pageSize)
         .then(({ data, totalCount }: OptionsTypePage) => {
@@ -361,39 +370,56 @@
           setTotalCount(totalCount);
         })
         .catch(onError)
-        .finally(() => setLoading(false));
+        .finally(() => {
+          setIsLoading(false);
+          setIsTyping(false);
+        });
     },
     [options],
   );
 
-  const handleOnSearch = debounce((search: string) => {
-    const searchValue = search.trim();
-    // enables option creation
-    if (allowNewOptions && isSingleMode) {
-      const firstOption = selectOptions.length > 0 && selectOptions[0].value;
-      // replaces the last search value entered with the new one
-      // only when the value wasn't part of the original options
-      if (
-        searchValue &&
-        firstOption === searchedValue &&
-        !initialOptions.find(o => o.value === searchedValue)
-      ) {
-        selectOptions.shift();
-        setSelectOptions(selectOptions);
-      }
-      if (searchValue && !hasOption(searchValue, selectOptions)) {
-        const newOption = {
-          label: searchValue,
-          value: searchValue,
-        };
-        // adds a custom option
-        const newOptions = [...selectOptions, newOption];
-        setSelectOptions(newOptions);
-        setSelectValue(searchValue);
-      }
-    }
-    setSearchedValue(searchValue);
-  }, DEBOUNCE_TIMEOUT);
+  const handleOnSearch = useMemo(
+    () =>
+      debounce((search: string) => {
+        const searchValue = search.trim();
+        // enables option creation
+        if (allowNewOptions && isSingleMode) {
+          const firstOption =
+            selectOptions.length > 0 && selectOptions[0].value;
+          // replaces the last search value entered with the new one
+          // only when the value wasn't part of the original options
+          if (
+            searchValue &&
+            firstOption === searchedValue &&
+            !initialOptions.find(o => o.value === searchedValue)
+          ) {
+            selectOptions.shift();
+            setSelectOptions(selectOptions);
+          }
+          if (searchValue && !hasOption(searchValue, selectOptions)) {
+            const newOption = {
+              label: searchValue,
+              value: searchValue,
+            };
+            // adds a custom option
+            const newOptions = [...selectOptions, newOption];
+            setSelectOptions(newOptions);
+            setSelectValue(searchValue);
+          }
+        }
+        setSearchedValue(searchValue);
+        if (!searchValue) {
+          setIsTyping(false);
+        }
+      }, DEBOUNCE_TIMEOUT),
+    [
+      allowNewOptions,
+      initialOptions,
+      isSingleMode,
+      searchedValue,
+      selectOptions,
+    ],
+  );
 
   const handlePagination = (e: UIEvent<HTMLElement>) => {
     const vScroll = e.currentTarget;
@@ -469,9 +495,18 @@
     if (!isDropdownVisible) {
       originNode.ref?.current?.scrollTo({ top: 0 });
     }
+    if ((isLoading && selectOptions.length === 0) || isTyping) {
+      return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
+    }
     return error ? <Error error={error} /> : originNode;
   };
 
+  const onInputKeyDown = () => {
+    if (isAsync && !isTyping) {
+      setIsTyping(true);
+    }
+  };
+
   const SuffixIcon = () => {
     if (isLoading) {
       return <StyledSpin size="small" />;
@@ -496,6 +531,7 @@
         mode={mappedMode}
         onDeselect={handleOnDeselect}
         onDropdownVisibleChange={handleOnDropdownVisibleChange}
+        onInputKeyDown={onInputKeyDown}
         onPopupScroll={isAsync ? handlePagination : undefined}
         onSearch={shouldShowSearch ? handleOnSearch : undefined}
         onSelect={handleOnSelect}