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

villebro 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 8f070169a59 perf(datasource): add pagination to datasource editor 
tables to prevent browser freeze (#37555)
8f070169a59 is described below

commit 8f070169a59a8b84deabfb508a6bc0137a67181d
Author: madhushreeag <[email protected]>
AuthorDate: Mon Feb 23 15:19:33 2026 -0800

    perf(datasource): add pagination to datasource editor tables to prevent 
browser freeze (#37555)
    
    Co-authored-by: madhushree agarwal <[email protected]>
---
 .../Datasource/DatasourceModal/index.tsx           |   7 +-
 .../components/CollectionTable/index.tsx           |  55 ++-
 .../DatasourceEditor/DatasourceEditor.tsx          | 367 ++++++++++++---------
 .../DatasetUsageTab/DatasetUsageTab.test.tsx       | 143 ++++++++
 .../components/DatasetUsageTab/index.tsx           |  71 +++-
 .../tests/DatasourceEditor.test.tsx                | 242 ++++++++++++++
 .../src/components/Datasource/types.ts             |  11 +
 7 files changed, 721 insertions(+), 175 deletions(-)

diff --git 
a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx 
b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
index a6e3e84f949..f8b9f5dbb30 100644
--- a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
+++ b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
@@ -46,7 +46,9 @@ const DatasourceEditor = AsyncEsmComponent(
 
 const StyledDatasourceModal = styled(Modal)`
   && .ant-modal-content {
-    height: 900px;
+    max-height: none;
+    margin-top: 0;
+    margin-bottom: 0;
   }
 
   && .ant-modal-body {
@@ -363,6 +365,9 @@ const DatasourceModal: 
FunctionComponent<DatasourceModalProps> = ({
         </>
       }
       responsive
+      resizable
+      resizableConfig={{ defaultSize: { width: 'auto', height: '900px' } }}
+      draggable
     >
       <DatasourceEditor
         showLoadingForImport
diff --git 
a/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx
 
b/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx
index 40914d24a20..a99e7f344f5 100644
--- 
a/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx
@@ -84,12 +84,21 @@ export default class CRUDCollection extends PureComponent<
     const { collection, collectionArray } = createKeyedCollection(
       props.collection,
     );
+
+    // Get initial page size from pagination prop
+    const initialPageSize =
+      typeof props.pagination === 'object' && props.pagination?.pageSize
+        ? props.pagination.pageSize
+        : 10;
+
     this.state = {
       expandedColumns: {},
       collection,
       collectionArray,
       sortColumn: '',
       sort: 0,
+      currentPage: 1,
+      pageSize: initialPageSize,
     };
     this.onAddItem = this.onAddItem.bind(this);
     this.renderExpandableSection = this.renderExpandableSection.bind(this);
@@ -238,10 +247,19 @@ export default class CRUDCollection extends PureComponent<
   }
 
   handleTableChange(
-    _pagination: TablePaginationConfig,
+    pagination: TablePaginationConfig,
     _filters: Record<string, FilterValue | null>,
     sorter: SorterResult<CollectionItem> | SorterResult<CollectionItem>[],
   ) {
+    // Handle pagination changes
+    if (pagination.current !== undefined && pagination.pageSize !== undefined) 
{
+      this.setState({
+        currentPage: pagination.current,
+        pageSize: pagination.pageSize,
+      });
+    }
+
+    // Handle sorting changes
     const columnSorter = Array.isArray(sorter) ? sorter[0] : sorter;
     let newSortColumn = '';
     let newSortOrder = 0;
@@ -397,8 +415,22 @@ export default class CRUDCollection extends PureComponent<
       stickyHeader,
       emptyMessage = t('No items'),
       expandFieldset,
+      pagination = false,
+      filterTerm,
+      filterFields,
     } = this.props;
 
+    const displayData =
+      filterTerm && filterFields?.length
+        ? this.state.collectionArray.filter(item =>
+            filterFields.some(field =>
+              String(item[field] ?? '')
+                .toLowerCase()
+                .includes(filterTerm.toLowerCase()),
+            ),
+          )
+        : this.state.collectionArray;
+
     const tableColumns = this.buildTableColumns();
     const expandedRowKeys = Object.keys(this.state.expandedColumns).filter(
       id => this.state.expandedColumns[id],
@@ -416,6 +448,22 @@ export default class CRUDCollection extends PureComponent<
         }
       : undefined;
 
+    // Build controlled pagination config, clamping currentPage to valid range
+    // based on displayData (filtered) length, not the full collection
+    const { pageSize, currentPage: statePage } = this.state;
+    const totalItems = displayData.length;
+    const maxPage = totalItems > 0 ? Math.ceil(totalItems / pageSize) : 1;
+    const currentPage = Math.min(statePage, maxPage);
+    const paginationConfig: false | TablePaginationConfig | undefined =
+      pagination === false || pagination === undefined
+        ? pagination
+        : {
+            ...(typeof pagination === 'object' ? pagination : {}),
+            current: currentPage,
+            pageSize,
+            total: totalItems,
+          };
+
     return (
       <>
         <CrudButtonWrapper>
@@ -439,16 +487,15 @@ export default class CRUDCollection extends PureComponent<
         <Table<CollectionItem>
           data-test="crud-table"
           columns={tableColumns}
-          data={this.state.collectionArray as CollectionItem[]}
+          data={displayData as CollectionItem[]}
           rowKey={(record: CollectionItem) => String(record.id)}
           sticky={stickyHeader}
-          pagination={false}
+          pagination={paginationConfig}
           onChange={this.handleTableChange}
           locale={{ emptyText: emptyMessage }}
           css={
             stickyHeader &&
             css`
-              height: 350px;
               overflow: auto;
             `
           }
diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
index f5592a2c645..fe1a35cc72e 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
@@ -62,6 +62,7 @@ import {
   FormLabel,
   Icons,
   InfoTooltip,
+  Input,
   Loading,
   Row,
   Select,
@@ -274,6 +275,9 @@ interface DatasourceEditorState {
   datasourceType: string;
   usageCharts: ChartUsageData[];
   usageChartsCount: number;
+  metricSearchTerm: string;
+  columnSearchTerm: string;
+  calculatedColumnSearchTerm: string;
 }
 
 interface AbortControllers {
@@ -302,6 +306,8 @@ interface ColumnCollectionTableProps {
   className?: string;
   itemGenerator?: () => Partial<Column>;
   columnLabelTooltips?: Record<string, string>;
+  filterTerm?: string;
+  filterFields?: string[];
 }
 
 interface StackedFieldProps {
@@ -520,6 +526,8 @@ function ColumnCollectionTable({
     groupby: true,
   }),
   columnLabelTooltips,
+  filterTerm,
+  filterFields,
 }: ColumnCollectionTableProps): JSX.Element {
   return (
     <CollectionTable
@@ -552,6 +560,8 @@ function ColumnCollectionTable({
       itemGenerator={itemGenerator}
       collection={columns}
       columnLabelTooltips={columnLabelTooltips}
+      filterTerm={filterTerm}
+      filterFields={filterFields}
       stickyHeader
       expandFieldset={
         <FormContainer>
@@ -941,6 +951,9 @@ class DatasourceEditor extends PureComponent<
         : DATASOURCE_TYPES.physical.key,
       usageCharts: [],
       usageChartsCount: 0,
+      metricSearchTerm: '',
+      columnSearchTerm: '',
+      calculatedColumnSearchTerm: '',
     };
 
     this.isComponentMounted = false;
@@ -2111,171 +2124,187 @@ class DatasourceEditor extends PureComponent<
   }
 
   renderMetricCollection() {
-    const { datasource } = this.state;
+    const { datasource, metricSearchTerm } = this.state;
     const { metrics } = datasource;
     const sortedMetrics = metrics?.length ? this.sortMetrics(metrics) : [];
     return (
-      <CollectionTable
-        tableColumns={['metric_name', 'verbose_name', 'expression']}
-        sortColumns={['metric_name', 'verbose_name', 'expression']}
-        columnLabels={{
-          metric_name: t('Metric Key'),
-          verbose_name: t('Label'),
-          expression: t('SQL expression'),
-        }}
-        columnLabelTooltips={{
-          metric_name: t(
-            'This field is used as a unique identifier to attach ' +
-              'the metric to charts. It is also used as the alias in the ' +
-              'SQL query.',
-          ),
-        }}
-        expandFieldset={
-          <FormContainer>
-            <Fieldset compact>
-              <Field
-                fieldKey="description"
-                label={t('Description')}
-                control={
-                  <TextControl
-                    controlId="description"
-                    placeholder={t('Description')}
-                  />
-                }
-              />
-              <Field
-                fieldKey="d3format"
-                label={t('D3 format')}
-                control={
-                  <TextControl controlId="d3format" placeholder="%y/%m/%d" />
-                }
-              />
-              <Field
-                fieldKey="currency"
-                label={t('Metric currency')}
-                control={
-                  <CurrencyControl
-                    onChange={() => {}}
-                    currencySelectOverrideProps={{
-                      placeholder: t('Select or type currency symbol'),
-                    }}
-                    symbolSelectAdditionalStyles={css`
-                      max-width: 30%;
-                    `}
+      <div>
+        <Input.Search
+          placeholder={t('Search metrics by key or label')}
+          value={metricSearchTerm}
+          onChange={e => this.setState({ metricSearchTerm: e.target.value })}
+          style={{ marginBottom: 16, width: 300 }}
+          allowClear
+        />
+        <CollectionTable
+          tableColumns={['metric_name', 'verbose_name', 'expression']}
+          sortColumns={['metric_name', 'verbose_name', 'expression']}
+          filterTerm={metricSearchTerm}
+          filterFields={['metric_name', 'verbose_name']}
+          columnLabels={{
+            metric_name: t('Metric Key'),
+            verbose_name: t('Label'),
+            expression: t('SQL expression'),
+          }}
+          columnLabelTooltips={{
+            metric_name: t(
+              'This field is used as a unique identifier to attach ' +
+                'the metric to charts. It is also used as the alias in the ' +
+                'SQL query.',
+            ),
+          }}
+          pagination={{
+            pageSize: 25,
+            showSizeChanger: true,
+            pageSizeOptions: [10, 25, 50, 100],
+          }}
+          expandFieldset={
+            <FormContainer>
+              <Fieldset compact>
+                <Field
+                  fieldKey="description"
+                  label={t('Description')}
+                  control={
+                    <TextControl
+                      controlId="description"
+                      placeholder={t('Description')}
+                    />
+                  }
+                />
+                <Field
+                  fieldKey="d3format"
+                  label={t('D3 format')}
+                  control={
+                    <TextControl controlId="d3format" placeholder="%y/%m/%d" />
+                  }
+                />
+                <Field
+                  fieldKey="currency"
+                  label={t('Metric currency')}
+                  control={
+                    <CurrencyControl
+                      onChange={() => {}}
+                      currencySelectOverrideProps={{
+                        placeholder: t('Select or type currency symbol'),
+                      }}
+                      symbolSelectAdditionalStyles={css`
+                        max-width: 30%;
+                      `}
+                    />
+                  }
+                />
+                <Field
+                  label={t('Certified by')}
+                  fieldKey="certified_by"
+                  description={t(
+                    'Person or group that has certified this metric',
+                  )}
+                  control={
+                    <TextControl
+                      controlId="certified_by"
+                      placeholder={t('Certified by')}
+                    />
+                  }
+                />
+                <Field
+                  label={t('Certification details')}
+                  fieldKey="certification_details"
+                  description={t('Details of the certification')}
+                  control={
+                    <TextControl
+                      controlId="certification_details"
+                      placeholder={t('Certification details')}
+                    />
+                  }
+                />
+                <Field
+                  label={t('Warning')}
+                  fieldKey="warning_markdown"
+                  description={t('Optional warning about use of this metric')}
+                  control={
+                    <TextAreaControl
+                      controlId="warning_markdown"
+                      language="markdown"
+                      offerEditInModal={false}
+                      resize="vertical"
+                    />
+                  }
+                />
+              </Fieldset>
+            </FormContainer>
+          }
+          collection={sortedMetrics}
+          allowAddItem
+          onChange={this.onDatasourcePropChange.bind(this, 'metrics')}
+          itemGenerator={() => ({
+            metric_name: t('<new metric>'),
+            verbose_name: '',
+            expression: '',
+          })}
+          itemCellProps={{
+            expression: () => ({
+              width: '240px',
+            }),
+          }}
+          itemRenderers={{
+            metric_name: (v, onChange, _, record) => (
+              <FlexRowContainer>
+                {record.is_certified && (
+                  <CertifiedBadge
+                    certifiedBy={record.certified_by}
+                    details={record.certification_details}
                   />
-                }
-              />
-              <Field
-                label={t('Certified by')}
-                fieldKey="certified_by"
-                description={t(
-                  'Person or group that has certified this metric',
                 )}
-                control={
-                  <TextControl
-                    controlId="certified_by"
-                    placeholder={t('Certified by')}
+                {record.warning_markdown && (
+                  <WarningIconWithTooltip
+                    warningMarkdown={record.warning_markdown}
                   />
-                }
+                )}
+                <EditableTitle
+                  canEdit
+                  title={v as string}
+                  onSaveTitle={onChange}
+                  maxWidth={300}
+                />
+              </FlexRowContainer>
+            ),
+            verbose_name: (v, onChange) => (
+              <TextControl value={v as string} onChange={onChange} />
+            ),
+            expression: (v, onChange) => (
+              <TextAreaControl
+                canEdit
+                initialValue={v as string}
+                onChange={onChange}
+                extraClasses={['datasource-sql-expression']}
+                language="sql"
+                offerEditInModal={false}
+                minLines={5}
+                textAreaStyles={{ minWidth: '200px', maxWidth: '450px' }}
+                resize="both"
               />
-              <Field
-                label={t('Certification details')}
-                fieldKey="certification_details"
-                description={t('Details of the certification')}
-                control={
-                  <TextControl
-                    controlId="certification_details"
-                    placeholder={t('Certification details')}
-                  />
+            ),
+            description: (v, onChange, label) => (
+              <StackedField
+                label={label}
+                formElement={
+                  <TextControl value={v as string} onChange={onChange} />
                 }
               />
-              <Field
-                label={t('Warning')}
-                fieldKey="warning_markdown"
-                description={t('Optional warning about use of this metric')}
-                control={
-                  <TextAreaControl
-                    controlId="warning_markdown"
-                    language="markdown"
-                    offerEditInModal={false}
-                    resize="vertical"
-                  />
+            ),
+            d3format: (v, onChange, label) => (
+              <StackedField
+                label={label}
+                formElement={
+                  <TextControl value={v as string} onChange={onChange} />
                 }
               />
-            </Fieldset>
-          </FormContainer>
-        }
-        collection={sortedMetrics}
-        allowAddItem
-        onChange={this.onDatasourcePropChange.bind(this, 'metrics')}
-        itemGenerator={() => ({
-          metric_name: t('<new metric>'),
-          verbose_name: '',
-          expression: '',
-        })}
-        itemCellProps={{
-          expression: () => ({
-            width: '240px',
-          }),
-        }}
-        itemRenderers={{
-          metric_name: (v, onChange, _, record) => (
-            <FlexRowContainer>
-              {record.is_certified && (
-                <CertifiedBadge
-                  certifiedBy={record.certified_by}
-                  details={record.certification_details}
-                />
-              )}
-              {record.warning_markdown && (
-                <WarningIconWithTooltip
-                  warningMarkdown={record.warning_markdown}
-                />
-              )}
-              <EditableTitle
-                canEdit
-                title={v as string}
-                onSaveTitle={onChange}
-                maxWidth={300}
-              />
-            </FlexRowContainer>
-          ),
-          verbose_name: (v, onChange) => (
-            <TextControl value={v as string} onChange={onChange} />
-          ),
-          expression: (v, onChange) => (
-            <TextAreaControl
-              canEdit
-              initialValue={v as string}
-              onChange={onChange}
-              extraClasses={['datasource-sql-expression']}
-              language="sql"
-              offerEditInModal={false}
-              minLines={5}
-              textAreaStyles={{ minWidth: '200px', maxWidth: '450px' }}
-              resize="both"
-            />
-          ),
-          description: (v, onChange, label) => (
-            <StackedField
-              label={label}
-              formElement={
-                <TextControl value={v as string} onChange={onChange} />
-              }
-            />
-          ),
-          d3format: (v, onChange, label) => (
-            <StackedField
-              label={label}
-              formElement={
-                <TextControl value={v as string} onChange={onChange} />
-              }
-            />
-          ),
-        }}
-        allowDeletes
-        stickyHeader
-      />
+            ),
+          }}
+          allowDeletes
+          stickyHeader
+        />
+      </div>
     );
   }
 
@@ -2332,9 +2361,6 @@ class DatasourceEditor extends PureComponent<
               children: (
                 <StyledTableTabWrapper>
                   {this.renderDefaultColumnSettings()}
-                  <DefaultColumnSettingsTitle>
-                    {t('Column Settings')}
-                  </DefaultColumnSettingsTitle>
                   <ColumnButtonWrapper>
                     <StyledButtonWrapper>
                       <Button
@@ -2349,9 +2375,20 @@ class DatasourceEditor extends PureComponent<
                       </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 })
@@ -2373,11 +2410,21 @@ class DatasourceEditor extends PureComponent<
               children: (
                 <StyledTableTabWrapper>
                   {this.renderDefaultColumnSettings()}
-                  <DefaultColumnSettingsTitle>
-                    {t('Column Settings')}
-                  </DefaultColumnSettingsTitle>
+                  <Input.Search
+                    placeholder={t('Search calculated columns by name')}
+                    value={this.state.calculatedColumnSearchTerm}
+                    onChange={e =>
+                      this.setState({
+                        calculatedColumnSearchTerm: e.target.value,
+                      })
+                    }
+                    style={{ marginBottom: 16, width: 300 }}
+                    allowClear
+                  />
                   <ColumnCollectionTable
                     columns={this.state.calculatedColumns}
+                    filterTerm={this.state.calculatedColumnSearchTerm}
+                    filterFields={['column_name']}
                     onColumnsChange={calculatedColumns =>
                       this.setColumns({ calculatedColumns })
                     }
diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx
index 103964fd7c2..5695dc2e277 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/DatasetUsageTab.test.tsx
@@ -547,3 +547,146 @@ test('handles AbortError without setState after unmount', 
async () => {
 
   consoleErrorSpy.mockRestore();
 });
+
+test('can search charts by chart name', async () => {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.getByText('Test Chart 2')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+  expect(searchInput).toBeInTheDocument();
+
+  await userEvent.type(searchInput, 'Chart 1');
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.queryByText('Test Chart 2')).not.toBeInTheDocument();
+  });
+
+  await userEvent.clear(searchInput);
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.getByText('Test Chart 2')).toBeInTheDocument();
+  });
+});
+
+test('can search charts by owner name', async () => {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+
+  await userEvent.type(searchInput, 'Bob');
+
+  await waitFor(() => {
+    expect(screen.queryByText('Test Chart 1')).not.toBeInTheDocument();
+    expect(screen.getByText('Test Chart 2')).toBeInTheDocument();
+  });
+});
+
+test('can search charts by dashboard title', async () => {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+
+  await userEvent.type(searchInput, 'Test Dashboard');
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.queryByText('Test Chart 2')).not.toBeInTheDocument();
+  });
+});
+
+test('chart search is case-insensitive', async () => {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+
+  await userEvent.type(searchInput, 'CHART 1');
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.queryByText('Test Chart 2')).not.toBeInTheDocument();
+  });
+});
+
+test('shows No items when search has no results', async () => {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+
+  await userEvent.type(searchInput, 'nonexistent chart');
+
+  await waitFor(() => {
+    expect(screen.queryByText('Test Chart 1')).not.toBeInTheDocument();
+    expect(screen.queryByText('Test Chart 2')).not.toBeInTheDocument();
+    expect(screen.getByText('No items')).toBeInTheDocument();
+  });
+});
+
+test('hides pagination when searching and restores it when cleared', async () 
=> {
+  setupTest();
+
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.getByText('Test Chart 2')).toBeInTheDocument();
+  });
+
+  // Pagination is visible when not searching (check for page number listitem)
+  expect(screen.getByTitle('1')).toBeInTheDocument();
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search charts by name, owner, or dashboard',
+  );
+
+  await userEvent.type(searchInput, 'Chart 1');
+
+  // Only matching chart is shown
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.queryByText('Test Chart 2')).not.toBeInTheDocument();
+  });
+
+  // Pagination is hidden while searching
+  expect(screen.queryByTitle('Next Page')).not.toBeInTheDocument();
+
+  await userEvent.clear(searchInput);
+
+  // Both charts are visible again after clearing search
+  await waitFor(() => {
+    expect(screen.getByText('Test Chart 1')).toBeInTheDocument();
+    expect(screen.getByText('Test Chart 2')).toBeInTheDocument();
+  });
+
+  // Pagination is restored
+  expect(screen.getByTitle('1')).toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/index.tsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/index.tsx
index 05bd1aa65d7..85b52f5f08f 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/index.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/components/DatasetUsageTab/index.tsx
@@ -19,7 +19,11 @@
 import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
 import { t } from '@apache-superset/core';
 import { styled, css } from '@apache-superset/core/ui';
-import { CertifiedBadge, InfoTooltip } from '@superset-ui/core/components';
+import {
+  CertifiedBadge,
+  InfoTooltip,
+  Input,
+} from '@superset-ui/core/components';
 import Table, {
   TableSize,
   SortOrder,
@@ -116,6 +120,7 @@ const DatasetUsageTab = ({
     'changed_on_delta_humanized',
   );
   const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
+  const [searchTerm, setSearchTerm] = useState('');
 
   const handleFetchCharts = useCallback(
     async (page = 1, column = sortColumn, direction = sortDirection) => {
@@ -289,20 +294,66 @@ const DatasetUsageTab = ({
     [handleSortChange, sortColumn, sortDirection],
   );
 
+  const filteredCharts = useMemo(() => {
+    if (!searchTerm) return charts;
+
+    const lowerSearch = searchTerm.toLowerCase();
+    return charts.filter(chart => {
+      // Search in chart name
+      if (chart.slice_name?.toLowerCase().includes(lowerSearch)) return true;
+
+      // Search in owner names
+      if (
+        chart.owners?.some(
+          owner =>
+            owner.first_name?.toLowerCase().includes(lowerSearch) ||
+            owner.last_name?.toLowerCase().includes(lowerSearch),
+        )
+      )
+        return true;
+
+      // Search in dashboard titles
+      if (
+        chart.dashboards?.some(dashboard =>
+          dashboard.dashboard_title?.toLowerCase().includes(lowerSearch),
+        )
+      )
+        return true;
+
+      return false;
+    });
+  }, [charts, searchTerm]);
+
   return (
     <div ref={tableContainerRef}>
+      <Input.Search
+        placeholder={t('Search charts by name, owner, or dashboard')}
+        value={searchTerm}
+        onChange={e => {
+          setSearchTerm(e.target.value);
+          if (!e.target.value) {
+            setCurrentPage(1);
+          }
+        }}
+        style={{ marginBottom: 16, width: 400 }}
+        allowClear
+      />
       <Table
         sticky
         columns={columns}
-        data={charts}
-        pagination={{
-          current: currentPage,
-          total: totalCount,
-          pageSize: PAGE_SIZE,
-          onChange: handlePageChange,
-          showSizeChanger: false,
-          size: 'default',
-        }}
+        data={filteredCharts}
+        pagination={
+          searchTerm
+            ? false
+            : {
+                current: currentPage,
+                total: totalCount,
+                pageSize: PAGE_SIZE,
+                onChange: handlePageChange,
+                showSizeChanger: false,
+                size: 'default',
+              }
+        }
         loading={loading}
         size={TableSize.Middle}
         rowKey={(record: Chart) =>
diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
index a2da3a5544c..a4bb7733337 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditor.test.tsx
@@ -579,3 +579,245 @@ test('immediate unmount after mount does not cause 
unhandled rejection from init
 
   consoleErrorSpy.mockRestore();
 });
+
+test('can search metrics by metric name', async () => {
+  const testProps = createProps();
+  await asyncRender(testProps);
+
+  // Navigate to Metrics tab
+  const metricsTab = screen.getByTestId('collection-tab-Metrics');
+  await userEvent.click(metricsTab);
+
+  // Find the search input
+  const searchInput = screen.getByPlaceholderText(
+    'Search metrics by key or label',
+  );
+  expect(searchInput).toBeInTheDocument();
+
+  // Verify initial metrics are shown
+  expect(screen.getByText('sum__num')).toBeInTheDocument();
+  expect(screen.getByText('avg__num')).toBeInTheDocument();
+
+  // Search for a specific metric
+  await userEvent.type(searchInput, 'sum');
+
+  // Verify filtered results
+  expect(screen.getByText('sum__num')).toBeInTheDocument();
+  expect(screen.queryByText('avg__num')).not.toBeInTheDocument();
+
+  // Clear search
+  await userEvent.clear(searchInput);
+
+  // Verify all metrics are shown again
+  await waitFor(() => {
+    expect(screen.getByText('sum__num')).toBeInTheDocument();
+    expect(screen.getByText('avg__num')).toBeInTheDocument();
+  });
+});
+
+test('can search metrics by verbose name', async () => {
+  const testProps = createProps();
+  await asyncRender(testProps);
+
+  // Navigate to Metrics tab
+  const metricsTab = screen.getByTestId('collection-tab-Metrics');
+  await userEvent.click(metricsTab);
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search metrics by key or label',
+  );
+
+  // Search by verbose name
+  await userEvent.type(searchInput, 'avg');
+
+  // Verify filtered results
+  await waitFor(() => {
+    expect(screen.queryByText('sum__num')).not.toBeInTheDocument();
+    expect(screen.getByText('avg__num')).toBeInTheDocument();
+  });
+});
+
+test('metric search is case-insensitive', async () => {
+  const testProps = createProps();
+  await asyncRender(testProps);
+
+  const metricsTab = screen.getByTestId('collection-tab-Metrics');
+  await userEvent.click(metricsTab);
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search metrics by key or label',
+  );
+
+  // Search with uppercase
+  await userEvent.type(searchInput, 'SUM');
+
+  // Verify results are still found
+  await waitFor(() => {
+    expect(screen.getByText('sum__num')).toBeInTheDocument();
+  });
+});
+
+test('can search columns by name', async () => {
+  const testProps = createProps();
+  await asyncRender(testProps);
+
+  // Navigate to Columns tab
+  const columnsTab = screen.getByTestId('collection-tab-Columns');
+  await userEvent.click(columnsTab);
+
+  // Find the search input
+  const searchInput = screen.getByPlaceholderText('Search columns by name');
+  expect(searchInput).toBeInTheDocument();
+
+  // Get initial column count
+  const initialColumns = screen.getAllByRole('row');
+
+  // Search for a specific column
+  await userEvent.type(searchInput, 'ds');
+
+  // Verify filtered results (fewer rows than before)
+  await waitFor(() => {
+    const filteredColumns = screen.getAllByRole('row');
+    expect(filteredColumns.length).toBeLessThan(initialColumns.length);
+  });
+
+  // Clear search
+  await userEvent.clear(searchInput);
+
+  // Verify all columns are shown again
+  await waitFor(() => {
+    const allColumns = screen.getAllByRole('row');
+    expect(allColumns.length).toBe(initialColumns.length);
+  });
+});
+
+test('column search is case-insensitive', async () => {
+  const testProps = createProps();
+  await asyncRender(testProps);
+
+  const columnsTab = screen.getByTestId('collection-tab-Columns');
+  await userEvent.click(columnsTab);
+
+  const searchInput = screen.getByPlaceholderText('Search columns by name');
+
+  // Search with uppercase
+  await userEvent.type(searchInput, 'DS');
+
+  // Verify results are still found
+  await waitFor(() => {
+    const filteredColumns = screen.getAllByRole('row');
+    // Should have fewer rows than total (header + filtered rows)
+    expect(filteredColumns.length).toBeGreaterThan(0);
+  });
+});
+
+test('can search calculated columns by name', async () => {
+  const testProps = createProps();
+  // Add some calculated columns with expressions
+  testProps.datasource.columns = [
+    ...testProps.datasource.columns,
+    {
+      id: 999,
+      type: 'VARCHAR',
+      filterable: true,
+      is_dttm: false,
+      is_active: true,
+      expression: 'CASE WHEN state = "CA" THEN 1 ELSE 0 END',
+      groupby: true,
+      column_name: 'is_california',
+    },
+    {
+      id: 1000,
+      type: 'VARCHAR',
+      filterable: true,
+      is_dttm: false,
+      is_active: true,
+      expression: 'UPPER(name)',
+      groupby: true,
+      column_name: 'upper_name',
+    },
+  ];
+
+  await asyncRender(testProps);
+
+  // Navigate to Calculated Columns tab
+  const calculatedColumnsTab = screen.getByTestId(
+    'collection-tab-Calculated columns',
+  );
+  await userEvent.click(calculatedColumnsTab);
+
+  // Wait for calculated columns to be displayed
+  // column_name is rendered as an input (TextControl) since 
editableColumnName=true
+  await waitFor(() => {
+    expect(screen.getByDisplayValue('is_california')).toBeInTheDocument();
+  });
+
+  // Find the search input
+  const searchInput = screen.getByPlaceholderText(
+    'Search calculated columns by name',
+  );
+  expect(searchInput).toBeInTheDocument();
+
+  // Verify both columns are visible initially
+  expect(screen.getByDisplayValue('upper_name')).toBeInTheDocument();
+
+  // Search for a specific calculated column
+  await userEvent.type(searchInput, 'california');
+
+  // Verify filtered results
+  await waitFor(() => {
+    expect(screen.getByDisplayValue('is_california')).toBeInTheDocument();
+    expect(screen.queryByDisplayValue('upper_name')).not.toBeInTheDocument();
+  });
+
+  // Clear search
+  await userEvent.clear(searchInput);
+
+  // Verify all calculated columns are shown again
+  await waitFor(() => {
+    expect(screen.getByDisplayValue('is_california')).toBeInTheDocument();
+    expect(screen.getByDisplayValue('upper_name')).toBeInTheDocument();
+  });
+});
+
+test('calculated column search is case-insensitive', async () => {
+  const testProps = createProps();
+  testProps.datasource.columns = [
+    ...testProps.datasource.columns,
+    {
+      id: 999,
+      type: 'VARCHAR',
+      filterable: true,
+      is_dttm: false,
+      is_active: true,
+      expression: 'UPPER(name)',
+      groupby: true,
+      column_name: 'upper_name',
+    },
+  ];
+
+  await asyncRender(testProps);
+
+  const calculatedColumnsTab = screen.getByTestId(
+    'collection-tab-Calculated columns',
+  );
+  await userEvent.click(calculatedColumnsTab);
+
+  // Wait for calculated column to be displayed
+  // column_name is rendered as an input (TextControl) since 
editableColumnName=true
+  await waitFor(() => {
+    expect(screen.getByDisplayValue('upper_name')).toBeInTheDocument();
+  });
+
+  const searchInput = screen.getByPlaceholderText(
+    'Search calculated columns by name',
+  );
+
+  // Search with different case
+  await userEvent.type(searchInput, 'UPPER');
+
+  // Verify results are still found
+  await waitFor(() => {
+    expect(screen.getByDisplayValue('upper_name')).toBeInTheDocument();
+  });
+});
diff --git a/superset-frontend/src/components/Datasource/types.ts 
b/superset-frontend/src/components/Datasource/types.ts
index a9432b641b1..3d5dea5c659 100644
--- a/superset-frontend/src/components/Datasource/types.ts
+++ b/superset-frontend/src/components/Datasource/types.ts
@@ -79,6 +79,15 @@ export interface CRUDCollectionProps {
   tableLayout?: 'fixed' | 'auto';
   sortColumns: string[];
   stickyHeader?: boolean;
+  pagination?:
+    | boolean
+    | {
+        pageSize?: number;
+        showSizeChanger?: boolean;
+        pageSizeOptions?: number[];
+      };
+  filterTerm?: string;
+  filterFields?: string[];
 }
 
 export type Sort = number | string | boolean | any;
@@ -95,6 +104,8 @@ export interface CRUDCollectionState {
   expandedColumns: Record<PropertyKey, any>;
   sortColumn: string;
   sort: SortOrder;
+  currentPage: number;
+  pageSize: number;
 }
 
 export enum FoldersEditorItemType {


Reply via email to