codeant-ai-for-open-source[bot] commented on code in PR #37902:
URL: https://github.com/apache/superset/pull/37902#discussion_r2844238188


##########
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx:
##########
@@ -2306,294 +2231,233 @@ class DatasourceEditor extends PureComponent<
         />
       </div>
     );
-  }
+  }, [datasource, sortMetrics, onDatasourcePropChange, metricSearchTerm]);
 
-  render() {
-    const { datasource, activeTabKey } = this.state;
-    const { metrics } = datasource;
-    const sortedMetrics = metrics?.length ? this.sortMetrics(metrics) : [];
+  const sortedMetrics = useMemo(
+    () => (datasource.metrics?.length ? sortMetrics(datasource.metrics) : []),
+    [datasource.metrics, sortMetrics],
+  );
 
-    return (
-      <DatasourceContainer data-test="datasource-editor">
-        {this.renderErrors()}
-        <Alert
-          css={theme => ({ marginBottom: theme.sizeUnit * 4 })}
-          type="warning"
-          message={
-            <>
-              {' '}
-              <strong>{t('Be careful.')} </strong>
-              {t(
-                'Changing these settings will affect all charts using this 
dataset, including charts owned by other people.',
-              )}
-            </>
-          }
-        />
-        <StyledTableTabs
-          id="table-tabs"
-          data-test="edit-dataset-tabs"
-          onChange={this.handleTabSelect}
-          defaultActiveKey={activeTabKey}
-          items={[
-            {
-              key: TABS_KEYS.SOURCE,
-              label: t('Source'),
-              children: this.renderSourceFieldset(),
-            },
-            {
-              key: TABS_KEYS.METRICS,
-              label: (
-                <CollectionTabTitle
-                  collection={sortedMetrics}
-                  title={t('Metrics')}
-                />
-              ),
-              children: this.renderMetricCollection(),
-            },
-            {
-              key: TABS_KEYS.COLUMNS,
-              label: (
-                <CollectionTabTitle
-                  collection={this.state.databaseColumns}
-                  title={t('Columns')}
-                />
-              ),
-              children: (
-                <StyledTableTabWrapper>
-                  {this.renderDefaultColumnSettings()}
-                  <ColumnButtonWrapper>
-                    <StyledButtonWrapper>
-                      <Button
-                        buttonSize="small"
-                        buttonStyle="tertiary"
-                        onClick={this.syncMetadata}
-                        className="sync-from-source"
-                        disabled={this.state.isEditMode}
-                      >
-                        <Icons.DatabaseOutlined iconSize="m" />
-                        {t('Sync columns from source')}
-                      </Button>
-                    </StyledButtonWrapper>
-                  </ColumnButtonWrapper>
-                  <Input.Search
-                    placeholder={t('Search columns by name')}
-                    value={this.state.columnSearchTerm}
-                    onChange={e =>
-                      this.setState({ columnSearchTerm: e.target.value })
-                    }
-                    style={{ marginBottom: 16, width: 300 }}
-                    allowClear
-                  />
-                  <ColumnCollectionTable
-                    className="columns-table"
-                    columns={this.state.databaseColumns}
-                    filterTerm={this.state.columnSearchTerm}
-                    filterFields={['column_name']}
-                    datasource={datasource}
-                    onColumnsChange={databaseColumns =>
-                      this.setColumns({ databaseColumns })
-                    }
-                    onDatasourceChange={this.onDatasourceChange}
-                  />
-                  {this.state.metadataLoading && <Loading />}
-                </StyledTableTabWrapper>
-              ),
-            },
+  const tabItems = useMemo(
+    () => [
+      {
+        key: TABS_KEYS.SOURCE,
+        label: t('Source'),
+        children: renderSourceFieldset(),
+      },
+      {
+        key: TABS_KEYS.METRICS,
+        label: (
+          <CollectionTabTitle collection={sortedMetrics} title={t('Metrics')} 
/>
+        ),
+        children: renderMetricCollection(),
+      },
+      {
+        key: TABS_KEYS.COLUMNS,
+        label: (
+          <CollectionTabTitle
+            collection={databaseColumns}
+            title={t('Columns')}
+          />
+        ),
+        children: (
+          <StyledTableTabWrapper>
+            {renderDefaultColumnSettings()}
+            <DefaultColumnSettingsTitle>
+              {t('Column Settings')}
+            </DefaultColumnSettingsTitle>
+            <ColumnButtonWrapper>
+              <StyledButtonWrapper>
+                <Button
+                  buttonSize="small"
+                  buttonStyle="tertiary"
+                  onClick={syncMetadata}
+                  className="sync-from-source"
+                  disabled={isEditMode}

Review Comment:
   **Suggestion:** The "Sync columns from source" button is disabled when edit 
mode is enabled (`disabled={isEditMode}`), which inverts the intended behavior 
and prevents users from syncing metadata at the only time they can actually 
edit the dataset, effectively making the sync action unavailable during 
editing. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Users cannot sync columns while dataset is in edit.
   - ⚠️ Edit lock semantics inconsistent with other edit-only controls.
   - ⚠️ Confusing UX around when metadata synchronization is allowed.
   ```
   </details>
   
   ```suggestion
                     disabled={!isEditMode}
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open any dataset for editing via the dataset modal, which renders 
`DatasourceEditor`
   inside `DatasourceModal` at
   
`superset-frontend/src/components/Datasource/DatasourceModal/index.tsx:33-40` 
(verified
   with Read).
   
   2. In `DatasourceEditor`, note that edit locking is controlled by 
`isEditMode` in
   `renderSourceFieldset` at `DatasourceEditor.tsx:1719-1750`, where the lock 
icon toggles
   `isEditMode` via `onChangeEditMode`.
   
   3. Switch to the "Columns" tab, whose content is defined in the `tabItems` 
useMemo at
   `DatasourceEditor.tsx:2241-2290`; observe the "Sync columns from source" 
button rendered
   at `DatasourceEditor.tsx:2271-2279` with `disabled={isEditMode}`.
   
   4. Click the lock icon to enable edit mode (`isEditMode` becomes true, 
enabling other
   editing controls like the datasource type radios at 
`DatasourceEditor.tsx:1719-1727`), and
   observe that the "Sync columns from source" button is now disabled due to
   `disabled={isEditMode}`, preventing metadata sync at precisely the time when 
the user is
   allowed to edit dataset columns.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
   **Line:** 2276:2276
   **Comment:**
        *Logic Error: The "Sync columns from source" button is disabled when 
edit mode is enabled (`disabled={isEditMode}`), which inverts the intended 
behavior and prevents users from syncing metadata at the only time they can 
actually edit the dataset, effectively making the sync action unavailable 
during editing.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=9dded7dffd05467a6569b98d55c2d92b220e555c4f34d24f191a6e50f1c56eb6&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=9dded7dffd05467a6569b98d55c2d92b220e555c4f34d24f191a6e50f1c56eb6&reaction=dislike'>👎</a>



##########
superset-frontend/src/explore/components/controls/SelectControl.tsx:
##########
@@ -162,93 +149,120 @@ export const innerGetOptions = (props: 
SelectControlProps): SelectOption[] => {
       return { value: c as unknown as string | number, label: String(c) };
     });
   }
-  return options;
+  return selectOptions;
 };
 
-export default class SelectControl extends PureComponent<
-  SelectControlProps,
-  SelectControlState
-> {
-  static defaultProps = defaultProps;
+function SelectControl({
+  ariaLabel,
+  autoFocus = false,
+  choices = [],
+  clearable = true,
+  description = null,
+  disabled = false,
+  freeForm = false,
+  isLoading = false,
+  mode,
+  multi = false,
+  isMulti,
+  name,
+  onChange = () => {},
+  onFocus = () => {},
+  onSelect,
+  onDeselect,
+  value,
+  default: defaultValue,
+  showHeader = true,
+  optionRenderer,
+  valueKey = 'value',
+  options: optionsProp,
+  placeholder,
+  filterOption,
+  tokenSeparators,
+  notFoundContent,
+  label = undefined,
+  renderTrigger,
+  validationErrors,
+  rightNode,
+  leftNode,
+  onClick,
+  hovered,
+  tooltipOnClick,
+  warning,
+  danger,
+  sortComparator,
+}: SelectControlProps) {
+  const [options, setOptions] = useState<SelectOption[]>(() =>
+    innerGetOptions({
+      choices,
+      optionRenderer,
+      valueKey,
+      options: optionsProp,
+      name,
+    }),
+  );
 
-  constructor(props: SelectControlProps) {
-    super(props);
-    this.state = {
-      options: this.getOptions(props),
-    };
-    this.onChange = this.onChange.bind(this);
-    this.handleFilterOptions = this.handleFilterOptions.bind(this);
-  }
+  // Track previous choices/options for comparison
+  const prevChoicesRef = useRef(choices);
+  const prevOptionsRef = useRef(optionsProp);
 
-  componentDidUpdate(prevProps: SelectControlProps) {
+  useEffect(() => {
     if (
-      !isEqualArray(this.props.choices, prevProps.choices) ||
-      !isEqualArray(this.props.options, prevProps.options)
+      !isEqualArray(choices, prevChoicesRef.current) ||
+      !isEqualArray(optionsProp, prevOptionsRef.current)
     ) {
-      const options = this.getOptions(this.props);
-      this.setState({ options });
+      const newOptions = innerGetOptions({
+        choices,
+        optionRenderer,
+        valueKey,
+        options: optionsProp,
+        name,
+      });
+      setOptions(newOptions);
+      prevChoicesRef.current = choices;
+      prevOptionsRef.current = optionsProp;
     }
-  }
+  }, [choices, optionsProp, optionRenderer, valueKey, name]);
 
   // Beware: This is acting like an on-click instead of an on-change
   // (firing every time user chooses vs firing only if a new option is chosen).
-  onChange(val: SelectValue | SelectOption | SelectOption[]) {
-    // will eventually call `exploreReducer`: SET_FIELD_VALUE
-    const { valueKey = 'value' } = this.props;
-    let onChangeVal: SelectValue = val as SelectValue;
+  const handleChange = useCallback(
+    (val: SelectValue | SelectOption | SelectOption[]) => {
+      // will eventually call `exploreReducer`: SET_FIELD_VALUE
+      let onChangeVal: SelectValue = val as SelectValue;
 
-    if (Array.isArray(val)) {
-      const values = val.map(v =>
-        typeof v === 'object' &&
-        v !== null &&
-        (v as SelectOption)[valueKey] !== undefined
-          ? (v as SelectOption)[valueKey]
-          : v,
-      );
-      onChangeVal = values as (string | number)[];
-    }
-    if (
-      typeof val === 'object' &&
-      val !== null &&
-      !Array.isArray(val) &&
-      (val as SelectOption)[valueKey] !== undefined
-    ) {
-      onChangeVal = (val as SelectOption)[valueKey] as string | number;
-    }
-    this.props.onChange?.(onChangeVal, []);
-  }
-
-  getOptions(props: SelectControlProps) {
-    return innerGetOptions(props);
-  }
+      if (Array.isArray(val)) {
+        const values = val.map(v =>
+          typeof v === 'object' &&
+          v !== null &&
+          (v as SelectOption)[valueKey] !== undefined
+            ? (v as SelectOption)[valueKey]
+            : v,
+        );
+        onChangeVal = values as (string | number)[];
+      }
+      if (
+        typeof val === 'object' &&
+        val !== null &&
+        !Array.isArray(val) &&
+        (val as SelectOption)[valueKey] !== undefined
+      ) {
+        onChangeVal = (val as SelectOption)[valueKey] as string | number;
+      }
+      onChange?.(onChangeVal, []);
+    },
+    [onChange, valueKey],
+  );
 
-  handleFilterOptions(text: string, option: SelectOption) {
-    const { filterOption } = this.props;
-    return filterOption?.({ data: option }, text) ?? true;
-  }
+  const handleFilterOptions = useCallback(
+    (text: string, option: SelectOption) =>
+      filterOption?.({ data: option }, text) ?? true,

Review Comment:
   **Suggestion:** The `handleFilterOptions` adapter passes arguments to the 
user-provided `filterOption` callback in the wrong order, so callers that 
expect `(input, option)` (where `input` is the search text and `option` is the 
option object) will instead receive the option object as the first argument and 
the search text as the second, which can break custom filters (e.g., calling 
string methods on a non-string) and cause incorrect or failing option 
filtering. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Custom-filtered Explore selects can throw runtime errors.
   - ⚠️ Search behavior in affected controls becomes incorrect or inconsistent.
   ```
   </details>
   
   ```suggestion
         filterOption?.(text, { data: option }) ?? true,
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In any test or storybook, render `SelectControl` from
   `superset-frontend/src/explore/components/controls/SelectControl.tsx` 
(function component
   declared at lines 155–193 in that file) with a custom `filterOption` prop 
implemented as
   `(input: string, option: { data: any }) => 
input.toLowerCase().includes('foo')`.
   
   2. Ensure `SelectControl` is given a non-empty `choices` or `options` prop 
so that
   `innerGetOptions` (lines 127–153) produces at least one `SelectOption`, and 
the internal
   `<Select>` from `@superset-ui/core/components` renders options with search 
enabled
   (default behavior in `Select.tsx` at lines 96–118 and 754–784).
   
   3. Type any text into the select's search box in the rendered UI; the 
underlying `Select`
   component calls the `filterOption` function passed in `selectProps` (built in
   `SelectControl` at lines 309–363) with arguments `(search: string, option: 
SelectOption)`,
   which are then forwarded to `handleFilterOptions` (lines 256–260).
   
   4. Observe that `handleFilterOptions` currently calls the user-provided 
`filterOption` as
   `filterOption?.({ data: option }, text)`, so the first parameter is the 
option object and
   the second is the search string, contrary to the `SelectControlProps` type 
`filterOption?:
   (input: unknown, option: unknown) => boolean;` on line 65; this leads to 
runtime errors
   (e.g., attempting `input.toLowerCase()` on a non-string) or incorrect 
filtering behavior
   when custom `filterOption` logic assumes the documented `(input, option)` 
order.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/explore/components/controls/SelectControl.tsx
   **Line:** 258:258
   **Comment:**
        *Logic Error: The `handleFilterOptions` adapter passes arguments to the 
user-provided `filterOption` callback in the wrong order, so callers that 
expect `(input, option)` (where `input` is the search text and `option` is the 
option object) will instead receive the option object as the first argument and 
the search text as the second, which can break custom filters (e.g., calling 
string methods on a non-string) and cause incorrect or failing option filtering.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=b7fcf0c1101f12199b1c5bbe71ba19ef6610b069cfe18b162c825ced942f65e6&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=b7fcf0c1101f12199b1c5bbe71ba19ef6610b069cfe18b162c825ced942f65e6&reaction=dislike'>👎</a>



##########
superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx:
##########
@@ -32,279 +32,310 @@ export interface DataEntry {
 }
 
 interface TTestTableProps {
-  alpha: number;
+  alpha?: number;
   data: DataEntry[];
   groups: string[];
-  liftValPrec: number;
+  liftValPrec?: number;
   metric: string;
-  pValPrec: number;
+  pValPrec?: number;
 }
 
-interface TTestTableState {
-  control: number;
-  liftValues: (string | number)[];
-  pValues: (string | number)[];
-}
+function TTestTable({
+  alpha = 0.05,
+  data,
+  groups,
+  liftValPrec = 4,
+  metric,
+  pValPrec = 6,
+}: TTestTableProps) {
+  const [control, setControl] = useState(0);
+  const [liftValues, setLiftValues] = useState<(string | number)[]>([]);
+  const [pValues, setPValues] = useState<(string | number)[]>([]);
 
-const defaultProps = {
-  alpha: 0.05,
-  liftValPrec: 4,
-  pValPrec: 6,
-};
+  const computeLift = useCallback(
+    (values: DataPointValue[], controlValues: DataPointValue[]): string => {
+      // Compute the lift value between two time series
+      let sumValues = 0;
+      let sumControl = 0;
+      values.forEach((value, i) => {
+        sumValues += value.y;
+        sumControl += controlValues[i].y;
+      });
 
-class TTestTable extends Component<TTestTableProps, TTestTableState> {
-  static defaultProps = defaultProps;
+      return (((sumValues - sumControl) / sumControl) * 100).toFixed(

Review Comment:
   **Suggestion:** The `computeLift` function divides by `sumControl` without 
guarding for the case where the control sum is 0, which will produce `Infinity` 
or `-Infinity` and then cause `toFixed` to throw a `RangeError` at runtime; 
this will break rendering when the control group has a total of zero. Add a 
zero-check to return a safe placeholder (e.g. `'NaN'`) when `sumControl` is 0 
so the rest of the component can handle it as an invalid value. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Paired T-test legacy chart can crash on zero-control data.
   - ❌ Dashboards using Paired T-test may fail to render.
   - ⚠️ Explore view breaks when querying sparse control-group metrics.
   - ⚠️ Reports/alerts using this viz may error on render.
   ```
   </details>
   
   ```suggestion
         // Avoid division by zero, which would produce Infinity and make 
toFixed throw
         if (sumControl === 0) {
           return 'NaN';
         }
   
   ```
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. In the Superset UI, create or open a chart using the "Paired TTest" 
visualization type
   (`VizType.PairedTTest = 'paired_ttest'` defined in
   `superset-frontend/packages/superset-ui-core/src/chart/types/VizType.ts:45` 
and registered
   in `superset-frontend/src/visualizations/presets/MainPreset.ts:30,135` via
   `PairedTTestChartPlugin`).
   
   2. Configure the chart so that the query returns, for at least one metric, a 
control group
   whose time series has all `y` values equal to `0` (e.g., no events in the 
control group),
   producing `DataEntry[]` with `values` arrays where every 
`controlValues[i].y` is `0`. This
   data shape is consumed by `PairedTTest` in
   
`superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx:23-31,109-132`,
   which passes `data[metric]` into `<TTestTable ... />`.
   
   3. When the Paired T-test chart renders, `TTestTable` mounts
   
(`superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx:43-50`),
   triggering its `useEffect` at lines `137-153` which calls 
`computeTTest(control)`
   (`108-135`), and in turn calls `computeLift(data[i].values, 
data[controlIndex].values)`
   (`117-127` and `55-70`) for each row.
   
   4. For the row where the selected control group's `values` all have `y === 
0`,
   `computeLift` computes `sumControl = 0` and then executes `(((sumValues - 
sumControl) /
   sumControl) * 100).toFixed(liftValPrec)` (`65-67`), which attempts `toFixed` 
on an
   infinite/invalid number and throws a `RangeError`, causing the React 
component for the
   Paired T-test chart to error and fail to render in dashboards, Explore, or 
reports when
   that data condition occurs.
   ```
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** 
superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx
   **Line:** 65:65
   **Comment:**
        *Logic Error: The `computeLift` function divides by `sumControl` 
without guarding for the case where the control sum is 0, which will produce 
`Infinity` or `-Infinity` and then cause `toFixed` to throw a `RangeError` at 
runtime; this will break rendering when the control group has a total of zero. 
Add a zero-check to return a safe placeholder (e.g. `'NaN'`) when `sumControl` 
is 0 so the rest of the component can handle it as an invalid value.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=e3aeafa0bf8219dff264e86a352e59240057ab1eb1fd0ae84b7a37c0b6f7c4cd&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F37902&comment_hash=e3aeafa0bf8219dff264e86a352e59240057ab1eb1fd0ae84b7a37c0b6f7c4cd&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to