You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by yo...@apache.org on 2021/03/12 04:32:11 UTC

[superset] branch master updated: feat: Implement drag and drop for metrics (#13575)

This is an automated email from the ASF dual-hosted git repository.

yongjiezhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d439da2  feat: Implement drag and drop for metrics (#13575)
d439da2 is described below

commit d439da2fe00c0a16edfec3fb5354d8e8f4da470b
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Fri Mar 12 05:31:17 2021 +0100

    feat: Implement drag and drop for metrics (#13575)
    
    * Convert MetricsControl to functional component
    
    * Implement drag and drop for metrics
    
    * Implement customizable placeholder for DndSelectLabel
---
 .../DndColumnSelectControl/DndFilterSelect.tsx     |  15 +-
 .../DndColumnSelectControl/DndMetricSelect.tsx     | 301 +++++++++++++++++++++
 .../DndColumnSelectControl/DndSelectLabel.tsx      |   2 +-
 .../controls/DndColumnSelectControl/index.ts       |   1 +
 .../controls/DndColumnSelectControl/types.ts       |   8 +-
 .../MetricControl/AdhocMetricPopoverTrigger.tsx    |  41 ++-
 .../MetricControl/MetricDefinitionValue.jsx        |  23 +-
 .../components/controls/MetricControl/types.ts     |   2 +-
 .../src/explore/components/controls/index.js       |   2 +
 9 files changed, 357 insertions(+), 38 deletions(-)

diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
index 5b14888..e835f99 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
@@ -17,12 +17,12 @@
  * under the License.
  */
 import React, { useEffect, useMemo, useState } from 'react';
-import { logging, SupersetClient } from '@superset-ui/core';
+import { logging, SupersetClient, t } from '@superset-ui/core';
 import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
 import { Tooltip } from 'src/common/components/Tooltip';
 import { OPERATORS } from 'src/explore/constants';
 import { OptionSortType } from 'src/explore/types';
-import { DndFilterSelectProps, FilterOptionValueType } from './types';
+import { DndFilterSelectProps, OptionValueType } from './types';
 import AdhocFilterPopoverTrigger from '../FilterControl/AdhocFilterPopoverTrigger';
 import OptionWrapper from './components/OptionWrapper';
 import DndSelectLabel from './DndSelectLabel';
@@ -37,13 +37,13 @@ import {
 } from '../../DatasourcePanel/types';
 import { DndItemType } from '../../DndItemType';
 
-const isDictionaryForAdhocFilter = (value: FilterOptionValueType) =>
+const isDictionaryForAdhocFilter = (value: OptionValueType) =>
   !(value instanceof AdhocFilter) && value?.expressionType;
 
 export const DndFilterSelect = (props: DndFilterSelectProps) => {
   const propsValues = Array.from(props.value ?? []);
   const [values, setValues] = useState(
-    propsValues.map((filter: FilterOptionValueType) =>
+    propsValues.map((filter: OptionValueType) =>
       isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
     ),
   );
@@ -144,7 +144,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
 
   useEffect(() => {
     setValues(
-      (props.value || []).map((filter: FilterOptionValueType) =>
+      (props.value || []).map((filter: OptionValueType) =>
         isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
       ),
     );
@@ -171,7 +171,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
       (savedMetric: Metric) => savedMetric.metric_name === savedMetricName,
     )?.expression;
 
-  const mapOption = (option: FilterOptionValueType) => {
+  const mapOption = (option: OptionValueType) => {
     // already a AdhocFilter, skip
     if (option instanceof AdhocFilter) {
       return option;
@@ -299,7 +299,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
 
   return (
     <>
-      <DndSelectLabel<FilterOptionValueType, FilterOptionValueType[]>
+      <DndSelectLabel<OptionValueType, OptionValueType[]>
         values={values}
         onDrop={(item: DatasourcePanelDndItem) => {
           setDroppedItem(item.value);
@@ -313,6 +313,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
           DndItemType.MetricOption,
           DndItemType.AdhocMetricOption,
         ]}
+        placeholderText={t('Drop columns or metrics')}
         {...props}
       />
       <AdhocFilterPopoverTrigger
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
new file mode 100644
index 0000000..7fe1b4b
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
@@ -0,0 +1,301 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with work for additional information
+ * regarding copyright ownership.  The ASF licenses file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { ensureIsArray, Metric, t } from '@superset-ui/core';
+import { ColumnMeta } from '@superset-ui/chart-controls';
+import { isEqual } from 'lodash';
+import { usePrevious } from 'src/common/hooks/usePrevious';
+import AdhocMetric from '../MetricControl/AdhocMetric';
+import AdhocMetricPopoverTrigger from '../MetricControl/AdhocMetricPopoverTrigger';
+import MetricDefinitionValue from '../MetricControl/MetricDefinitionValue';
+import { OptionValueType } from './types';
+import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
+import { DndItemType } from '../../DndItemType';
+import DndSelectLabel from './DndSelectLabel';
+import { savedMetricType } from '../MetricControl/types';
+
+const isDictionaryForAdhocMetric = (value: any) =>
+  value && !(value instanceof AdhocMetric) && value.expressionType;
+
+const coerceAdhocMetrics = (value: any) => {
+  if (!value) {
+    return [];
+  }
+  if (!Array.isArray(value)) {
+    if (isDictionaryForAdhocMetric(value)) {
+      return [new AdhocMetric(value)];
+    }
+    return [value];
+  }
+  return value.map(val => {
+    if (isDictionaryForAdhocMetric(val)) {
+      return new AdhocMetric(val);
+    }
+    return val;
+  });
+};
+
+const getOptionsForSavedMetrics = (
+  savedMetrics: savedMetricType[],
+  currentMetricValues: (string | AdhocMetric)[],
+  currentMetric?: string,
+) =>
+  savedMetrics?.filter(savedMetric =>
+    Array.isArray(currentMetricValues)
+      ? !currentMetricValues.includes(savedMetric.metric_name ?? '') ||
+        savedMetric.metric_name === currentMetric
+      : savedMetric,
+  ) ?? [];
+
+const columnsContainAllMetrics = (
+  value: (string | AdhocMetric | ColumnMeta)[],
+  columns: ColumnMeta[],
+  savedMetrics: savedMetricType[],
+) => {
+  const columnNames = new Set(
+    [...(columns || []), ...(savedMetrics || [])]
+      // eslint-disable-next-line camelcase
+      .map(
+        item =>
+          (item as ColumnMeta).column_name ||
+          (item as savedMetricType).metric_name,
+      ),
+  );
+
+  return (
+    ensureIsArray(value)
+      .filter(metric => metric)
+      // find column names
+      .map(metric =>
+        (metric as AdhocMetric).column
+          ? (metric as AdhocMetric).column.column_name
+          : (metric as ColumnMeta).column_name || metric,
+      )
+      .filter(name => name && typeof name === 'string')
+      .every(name => columnNames.has(name))
+  );
+};
+
+export const DndMetricSelect = (props: any) => {
+  const { onChange, multi, columns, savedMetrics } = props;
+
+  const handleChange = useCallback(
+    opts => {
+      // if clear out options
+      if (opts === null) {
+        onChange(null);
+        return;
+      }
+
+      const transformedOpts = ensureIsArray(opts);
+      const optionValues = transformedOpts
+        .map(option => {
+          // pre-defined metric
+          if (option.metric_name) {
+            return option.metric_name;
+          }
+          return option;
+        })
+        .filter(option => option);
+      onChange(multi ? optionValues : optionValues[0]);
+    },
+    [multi, onChange],
+  );
+
+  const [value, setValue] = useState<(AdhocMetric | Metric | string)[]>(
+    coerceAdhocMetrics(props.value),
+  );
+  const [droppedItem, setDroppedItem] = useState<DatasourcePanelDndItem | null>(
+    null,
+  );
+  const [newMetricPopoverVisible, setNewMetricPopoverVisible] = useState(false);
+  const prevColumns = usePrevious(columns);
+  const prevSavedMetrics = usePrevious(savedMetrics);
+
+  useEffect(() => {
+    setValue(coerceAdhocMetrics(props.value));
+  }, [JSON.stringify(props.value)]);
+
+  useEffect(() => {
+    if (
+      !isEqual(prevColumns, columns) ||
+      !isEqual(prevSavedMetrics, savedMetrics)
+    ) {
+      // Remove all metrics if selected value no longer a valid column
+      // in the dataset. Must use `nextProps` here because Redux reducers may
+      // have already updated the value for this control.
+      if (!columnsContainAllMetrics(props.value, columns, savedMetrics)) {
+        onChange([]);
+      }
+    }
+  }, [
+    prevColumns,
+    columns,
+    prevSavedMetrics,
+    savedMetrics,
+    props.value,
+    onChange,
+  ]);
+
+  const canDrop = (item: DatasourcePanelDndItem) => {
+    const isMetricAlreadyInValues =
+      item.type === 'metric' ? value.includes(item.value.metric_name) : false;
+    return (props.multi || value.length === 0) && !isMetricAlreadyInValues;
+  };
+
+  const onNewMetric = (newMetric: Metric) => {
+    const newValue = [...value, newMetric];
+    setValue(newValue);
+    handleChange(newValue);
+  };
+
+  const onMetricEdit = (
+    changedMetric: Metric | AdhocMetric,
+    oldMetric: Metric | AdhocMetric,
+  ) => {
+    const newValue = value.map(value => {
+      if (
+        // compare saved metrics
+        value === (oldMetric as Metric).metric_name ||
+        // compare adhoc metrics
+        typeof (value as AdhocMetric).optionName !== 'undefined'
+          ? (value as AdhocMetric).optionName ===
+            (oldMetric as AdhocMetric).optionName
+          : false
+      ) {
+        return changedMetric;
+      }
+      return value;
+    });
+    setValue(newValue);
+    handleChange(newValue);
+  };
+
+  const onRemoveMetric = (index: number) => {
+    if (!Array.isArray(value)) {
+      return;
+    }
+    const valuesCopy = [...value];
+    valuesCopy.splice(index, 1);
+    setValue(valuesCopy);
+    onChange(valuesCopy);
+  };
+
+  const moveLabel = (dragIndex: number, hoverIndex: number) => {
+    const newValues = [...value];
+    [newValues[hoverIndex], newValues[dragIndex]] = [
+      newValues[dragIndex],
+      newValues[hoverIndex],
+    ];
+    setValue(newValues);
+  };
+
+  const valueRenderer = (
+    option: Metric | AdhocMetric | string,
+    index: number,
+  ) => (
+    <MetricDefinitionValue
+      key={index}
+      index={index}
+      option={option}
+      onMetricEdit={onMetricEdit}
+      onRemoveMetric={() => onRemoveMetric(index)}
+      columns={props.columns}
+      savedMetrics={props.savedMetrics}
+      savedMetricsOptions={getOptionsForSavedMetrics(
+        props.savedMetrics,
+        props.value,
+        props.value?.[index],
+      )}
+      datasourceType={props.datasourceType}
+      onMoveLabel={moveLabel}
+      onDropLabel={() => onChange(value)}
+    />
+  );
+
+  const valuesRenderer = () =>
+    value.map((value, index) => valueRenderer(value, index));
+
+  const togglePopover = (visible: boolean) => {
+    setNewMetricPopoverVisible(visible);
+  };
+
+  const closePopover = () => {
+    togglePopover(false);
+  };
+
+  const { savedMetric, adhocMetric } = useMemo(() => {
+    if (droppedItem?.type === 'column') {
+      const itemValue = droppedItem?.value as ColumnMeta;
+      return {
+        savedMetric: {} as savedMetricType,
+        adhocMetric: new AdhocMetric({
+          column: { column_name: itemValue?.column_name },
+        }),
+      };
+    }
+    if (droppedItem?.type === 'metric') {
+      const itemValue = droppedItem?.value as savedMetricType;
+      return {
+        savedMetric: itemValue,
+        adhocMetric: new AdhocMetric({ isNew: true }),
+      };
+    }
+    return {
+      savedMetric: {} as savedMetricType,
+      adhocMetric: new AdhocMetric({ isNew: true }),
+    };
+  }, [droppedItem?.type, droppedItem?.value]);
+
+  return (
+    <div className="metrics-select">
+      <DndSelectLabel<OptionValueType, OptionValueType[]>
+        values={value}
+        onDrop={(item: DatasourcePanelDndItem) => {
+          setDroppedItem(item);
+          togglePopover(true);
+        }}
+        canDrop={canDrop}
+        valuesRenderer={valuesRenderer}
+        accept={[DndItemType.Column, DndItemType.Metric]}
+        placeholderText={t('Drop columns or metrics')}
+        {...props}
+      />
+      <AdhocMetricPopoverTrigger
+        adhocMetric={adhocMetric}
+        onMetricEdit={onNewMetric}
+        columns={props.columns}
+        savedMetricsOptions={getOptionsForSavedMetrics(
+          props.savedMetrics,
+          props.value,
+        )}
+        savedMetric={savedMetric}
+        datasourceType={props.datasourceType}
+        isControlledComponent
+        visible={newMetricPopoverVisible}
+        togglePopover={togglePopover}
+        closePopover={closePopover}
+        createNew
+      >
+        <div />
+      </AdhocMetricPopoverTrigger>
+    </div>
+  );
+};
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
index 820ae31..f0ee950 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
@@ -55,7 +55,7 @@ export default function DndSelectLabel<T, O>(
     return (
       <AddControlLabel cancelHover>
         <Icon name="plus-small" color={theme.colors.grayscale.light1} />
-        {t('Drop Columns')}
+        {t(props.placeholderText || 'Drop columns')}
       </AddControlLabel>
     );
   }
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts
index 7e8ca10..dda99f7 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts
@@ -19,3 +19,4 @@
 export { default } from './DndSelectLabel';
 export * from './DndColumnSelect';
 export * from './DndFilterSelect';
+export * from './DndMetricSelect';
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
index 13e30dd..68f830f 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
@@ -17,7 +17,6 @@
  * under the License.
  */
 import { ReactNode } from 'react';
-import { AdhocFilter } from '@superset-ui/core';
 import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
 import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
 import { DndItemType } from '../../DndItemType';
@@ -51,16 +50,17 @@ export interface DndColumnSelectProps<
   canDrop: (item: DatasourcePanelDndItem) => boolean;
   valuesRenderer: () => ReactNode;
   accept: DndItemType | DndItemType[];
+  placeholderText?: string;
 }
 
-export type FilterOptionValueType = Record<string, any> | AdhocFilter;
+export type OptionValueType = Record<string, any>;
 export interface DndFilterSelectProps {
   name: string;
-  value: FilterOptionValueType[];
+  value: OptionValueType[];
   columns: ColumnMeta[];
   datasource: Record<string, any>;
   formData: Record<string, any>;
   savedMetrics: Metric[];
-  onChange: (filters: FilterOptionValueType[]) => void;
+  onChange: (filters: OptionValueType[]) => void;
   options: { string: ColumnMeta };
 }
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
index dd7b4bf..4ca9c65 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
@@ -35,6 +35,10 @@ export type AdhocMetricPopoverTriggerProps = {
   datasourceType: string;
   children: ReactNode;
   createNew?: boolean;
+  isControlledComponent?: boolean;
+  visible?: boolean;
+  togglePopover?: (visible: boolean) => void;
+  closePopover?: () => void;
 };
 
 export type AdhocMetricPopoverTriggerState = {
@@ -139,7 +143,14 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
   }
 
   render() {
-    const { adhocMetric, savedMetric } = this.props;
+    const {
+      adhocMetric,
+      savedMetric,
+      columns,
+      savedMetricsOptions,
+      datasourceType,
+      isControlledComponent,
+    } = this.props;
     const { verbose_name, metric_name } = savedMetric;
     const { hasCustomLabel, label } = adhocMetric;
     const adhocMetricLabel = hasCustomLabel
@@ -152,16 +163,28 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
           hasCustomLabel,
         };
 
+    const { visible, togglePopover, closePopover } = isControlledComponent
+      ? {
+          visible: this.props.visible,
+          togglePopover: this.props.togglePopover,
+          closePopover: this.props.closePopover,
+        }
+      : {
+          visible: this.state.popoverVisible,
+          togglePopover: this.togglePopover,
+          closePopover: this.closePopover,
+        };
+
     const overlayContent = (
       <AdhocMetricEditPopover
         adhocMetric={adhocMetric}
         title={title}
-        columns={this.props.columns}
-        savedMetricsOptions={this.props.savedMetricsOptions}
-        savedMetric={this.props.savedMetric}
-        datasourceType={this.props.datasourceType}
+        columns={columns}
+        savedMetricsOptions={savedMetricsOptions}
+        savedMetric={savedMetric}
+        datasourceType={datasourceType}
         onResize={this.onPopoverResize}
-        onClose={this.closePopover}
+        onClose={closePopover}
         onChange={this.onChange}
         getCurrentTab={this.getCurrentTab}
         getCurrentLabel={this.getCurrentLabel}
@@ -181,9 +204,9 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
         placement="right"
         trigger="click"
         content={overlayContent}
-        defaultVisible={this.state.popoverVisible}
-        visible={this.state.popoverVisible}
-        onVisibleChange={this.togglePopover}
+        defaultVisible={visible}
+        visible={visible}
+        onVisibleChange={togglePopover}
         title={popoverTitle}
         destroyTooltipOnHide={this.props.createNew}
       >
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
index f3bb547..12dbd18 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
@@ -18,16 +18,20 @@
  */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Metric } from '@superset-ui/chart-controls/lib/types';
 import columnType from 'src/explore/propTypes/columnType';
-import { OptionControlLabel } from 'src/explore/components/OptionControls';
 import AdhocMetricOption from './AdhocMetricOption';
 import AdhocMetric from './AdhocMetric';
 import savedMetricType from './savedMetricType';
 import adhocMetricType from './adhocMetricType';
-import { DndItemType } from '../../DndItemType';
 
 const propTypes = {
-  option: PropTypes.oneOfType([savedMetricType, adhocMetricType]).isRequired,
+  option: PropTypes.oneOfType([
+    savedMetricType,
+    adhocMetricType,
+    Metric,
+    PropTypes.string,
+  ]).isRequired,
   index: PropTypes.number.isRequired,
   onMetricEdit: PropTypes.func,
   onRemoveMetric: PropTypes.func,
@@ -81,19 +85,6 @@ export default function MetricDefinitionValue({
 
     return <AdhocMetricOption {...metricOptionProps} />;
   }
-  if (typeof option === 'string') {
-    return (
-      <OptionControlLabel
-        label={option}
-        onRemove={onRemoveMetric}
-        onMoveLabel={onMoveLabel}
-        onDropLabel={onDropLabel}
-        type={DndItemType.FilterOption}
-        index={index}
-        isFunction
-      />
-    );
-  }
   return null;
 }
 MetricDefinitionValue.propTypes = propTypes;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/types.ts b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
index 0130353..f8f52fc 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
@@ -18,6 +18,6 @@
  */
 export type savedMetricType = {
   metric_name: string;
-  verbose_name: string;
+  verbose_name?: string;
   expression: string;
 };
diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js
index 28bfcd1..dc27b1a 100644
--- a/superset-frontend/src/explore/components/controls/index.js
+++ b/superset-frontend/src/explore/components/controls/index.js
@@ -42,6 +42,7 @@ import FilterBoxItemControl from './FilterBoxItemControl';
 import DndColumnSelectControl, {
   DndColumnSelect,
   DndFilterSelect,
+  DndMetricSelect,
 } from './DndColumnSelectControl';
 
 const controlMap = {
@@ -57,6 +58,7 @@ const controlMap = {
   DndColumnSelectControl,
   DndColumnSelect,
   DndFilterSelect,
+  DndMetricSelect,
   FixedOrMetricControl,
   HiddenControl,
   SelectAsyncControl,