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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6f56cd5  feat(listviews): SIP-34 filters for charts, dashboards, 
datasets (#10335)
6f56cd5 is described below

commit 6f56cd5e9d18ab8c89f282551b349ccf8da99dc1
Author: ʈᵃᵢ <tdupree...@gmail.com>
AuthorDate: Mon Jul 27 10:14:11 2020 -0700

    feat(listviews): SIP-34 filters for charts, dashboards, datasets (#10335)
---
 .../components/ListView/ListView_spec.jsx          | 233 ++++++----------
 .../{chartList => CRUD/chart}/ChartList_spec.jsx   |  14 +-
 .../dashboard}/DashboardList_spec.jsx              |  13 +-
 .../dataset}/DatasetList_spec.jsx                  |  14 +-
 .../ErrorMessage/TimeoutErrorMessage.tsx           |   2 +-
 .../src/components/ListView/LegacyFilters.tsx      | 204 --------------
 .../src/components/ListView/ListView.tsx           |  35 +--
 superset-frontend/src/components/ListView/types.ts |  30 +-
 superset-frontend/src/components/ListView/utils.ts |  23 --
 superset-frontend/src/components/Modal.tsx         |   2 +-
 superset-frontend/src/featureFlags.ts              |   1 -
 .../views/{chartList => CRUD/chart}/ChartList.tsx  | 303 +++++++--------------
 .../dashboard}/DashboardList.tsx                   | 271 ++++++------------
 .../dataset}/AddDatasetModal.tsx                   |  12 +-
 .../views/{datasetList => CRUD/dataset}/Button.tsx |   0
 .../{datasetList => CRUD/dataset}/DatasetList.tsx  | 302 +++++++++++---------
 superset-frontend/src/views/CRUD/utils.tsx         |  61 +++++
 superset-frontend/src/welcome/App.tsx              |   6 +-
 superset/config.py                                 |   1 -
 superset/databases/api.py                          |  90 +++++-
 superset/databases/schemas.py                      |  10 +
 tests/database_api_tests.py                        |  39 +++
 22 files changed, 678 insertions(+), 988 deletions(-)

diff --git 
a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx 
b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index de0d179..cd1f823 100644
--- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
@@ -19,7 +19,6 @@
 import React from 'react';
 import { mount, shallow } from 'enzyme';
 import { act } from 'react-dom/test-utils';
-import { MenuItem } from 'react-bootstrap';
 import { QueryParamProvider } from 'use-query-params';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
@@ -42,6 +41,7 @@ function makeMockLocation(query) {
   };
 }
 
+const fetchSelectsMock = jest.fn(() => []);
 const mockedProps = {
   title: 'Data Table',
   columns: [
@@ -61,9 +61,25 @@ const mockedProps = {
   ],
   filters: [
     {
+      Header: 'ID',
+      id: 'id',
+      input: 'select',
+      selects: [{ label: 'foo', value: 'bar' }],
+      operator: 'eq',
+    },
+    {
       Header: 'Name',
       id: 'name',
-      operators: [{ label: 'Starts With', value: 'sw' }],
+      input: 'search',
+      operator: 'ct',
+    },
+    {
+      Header: 'Age',
+      id: 'age',
+      input: 'select',
+      fetchSelects: fetchSelectsMock,
+      paginate: true,
+      operator: 'eq',
     },
   ],
   data: [
@@ -145,59 +161,6 @@ describe('ListView', () => {
                                     `);
   });
 
-  it('calls fetchData on filter', () => {
-    act(() => {
-      wrapper
-        .find('.dropdown-toggle')
-        .children('button')
-        .at(0)
-        .props()
-        .onClick();
-
-      wrapper
-        .find(MenuItem)
-        .at(0)
-        .props()
-        .onSelect({ id: 'name', Header: 'name' });
-    });
-    wrapper.update();
-
-    act(() => {
-      wrapper.find('.filter-inputs input[type="text"]').prop('onChange')({
-        persist() {},
-        currentTarget: { value: 'foo' },
-      });
-    });
-    wrapper.update();
-
-    act(() => {
-      wrapper.find('[data-test="apply-filters"]').last().prop('onClick')();
-    });
-    wrapper.update();
-
-    expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "filters": Array [
-      Object {
-        "id": "name",
-        "operator": "sw",
-        "value": "foo",
-      },
-    ],
-    "pageIndex": 0,
-    "pageSize": 1,
-    "sortBy": Array [
-      Object {
-        "desc": false,
-        "id": "id",
-      },
-    ],
-  },
-]
-`);
-  });
-
   it('renders pagination controls', () => {
     expect(wrapper.find(Pagination).exists()).toBe(true);
     expect(wrapper.find(Pagination.Prev).exists()).toBe(true);
@@ -212,26 +175,20 @@ Array [
     wrapper.update();
 
     expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "filters": Array [
-      Object {
-        "id": "name",
-        "operator": "sw",
-        "value": "foo",
-      },
-    ],
-    "pageIndex": 1,
-    "pageSize": 1,
-    "sortBy": Array [
-      Object {
-        "desc": false,
-        "id": "id",
-      },
-    ],
-  },
-]
-`);
+      Array [
+        Object {
+          "filters": Array [],
+          "pageIndex": 1,
+          "pageSize": 1,
+          "sortBy": Array [
+            Object {
+              "desc": false,
+              "id": "id",
+            },
+          ],
+        },
+      ]
+    `);
   });
 
   it('handles bulk actions on 1 row', () => {
@@ -339,46 +296,6 @@ Array [
       '"Invalid filter config, some_column is not present in columns"',
     );
   });
-});
-
-describe('ListView with new UI filters', () => {
-  const fetchSelectsMock = jest.fn(() => []);
-  const newFiltersProps = {
-    ...mockedProps,
-    isSIP34FilterUIEnabled: true,
-    filters: [
-      {
-        Header: 'ID',
-        id: 'id',
-        input: 'select',
-        selects: [{ label: 'foo', value: 'bar' }],
-        operator: 'eq',
-      },
-      {
-        Header: 'Name',
-        id: 'name',
-        input: 'search',
-        operator: 'ct',
-      },
-      {
-        Header: 'Age',
-        id: 'age',
-        input: 'select',
-        fetchSelects: fetchSelectsMock,
-        paginate: true,
-        operator: 'eq',
-      },
-    ],
-  };
-
-  const wrapper = factory(newFiltersProps);
-
-  afterEach(() => {
-    mockedProps.fetchData.mockClear();
-    mockedProps.bulkActions.forEach(ba => {
-      ba.onSelect.mockClear();
-    });
-  });
 
   it('renders UI filters', () => {
     expect(wrapper.find(ListViewFilters)).toHaveLength(1);
@@ -407,43 +324,53 @@ describe('ListView with new UI filters', () => {
       wrapper.find('[data-test="search-input"]').last().props().onBlur();
     });
 
-    expect(newFiltersProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "filters": Array [
-      Object {
-        "id": "id",
-        "operator": "eq",
-        "value": "bar",
-      },
-    ],
-    "pageIndex": 0,
-    "pageSize": 1,
-    "sortBy": Array [],
-  },
-]
-`);
-
-    expect(newFiltersProps.fetchData.mock.calls[1]).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "filters": Array [
-      Object {
-        "id": "id",
-        "operator": "eq",
-        "value": "bar",
-      },
-      Object {
-        "id": "name",
-        "operator": "ct",
-        "value": "something",
-      },
-    ],
-    "pageIndex": 0,
-    "pageSize": 1,
-    "sortBy": Array [],
-  },
-]
-`);
+    expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "filters": Array [
+            Object {
+              "id": "id",
+              "operator": "eq",
+              "value": "bar",
+            },
+          ],
+          "pageIndex": 0,
+          "pageSize": 1,
+          "sortBy": Array [
+            Object {
+              "desc": false,
+              "id": "id",
+            },
+          ],
+        },
+      ]
+    `);
+
+    expect(mockedProps.fetchData.mock.calls[1]).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "filters": Array [
+            Object {
+              "id": "id",
+              "operator": "eq",
+              "value": "bar",
+            },
+            Object {
+              "id": "name",
+              "operator": "ct",
+              "value": "something",
+            },
+          ],
+          "pageIndex": 0,
+          "pageSize": 1,
+          "sortBy": Array [
+            Object {
+              "desc": false,
+              "id": "id",
+            },
+          ],
+        },
+      ]
+    `);
   });
 });
diff --git 
a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx 
b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
similarity index 90%
rename from 
superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
rename to superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
index 695b74e..ba6c464 100644
--- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -23,7 +23,7 @@ import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
-import ChartList from 'src/views/chartList/ChartList';
+import ChartList from 'src/views/CRUD/chart/ChartList';
 import ListView from 'src/components/ListView/ListView';
 
 // store needed for withToasts(ChartTable)
@@ -48,13 +48,6 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
 
 fetchMock.get(chartsInfoEndpoint, {
   permissions: ['can_list', 'can_edit'],
-  filters: {
-    slice_name: [],
-    description: [],
-    viz_type: [],
-    datasource_name: [],
-    owners: [],
-  },
 });
 fetchMock.get(chartssOwnersEndpoint, {
   result: [],
@@ -95,11 +88,6 @@ describe('ChartList', () => {
     expect(callsI).toHaveLength(1);
   });
 
-  it('fetches owners', () => {
-    const callsO = fetchMock.calls(/chart\/related\/owners/);
-    expect(callsO).toHaveLength(1);
-  });
-
   it('fetches data', () => {
     wrapper.update();
     const callsD = fetchMock.calls(/chart\/\?q/);
diff --git 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
similarity index 91%
rename from 
superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
rename to 
superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
index cfaaeca..c847b17 100644
--- 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
+++ 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
@@ -23,7 +23,7 @@ import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
-import DashboardList from 'src/views/dashboardList/DashboardList';
+import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
 import ListView from 'src/components/ListView/ListView';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
 
@@ -50,12 +50,6 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
 
 fetchMock.get(dashboardsInfoEndpoint, {
   permissions: ['can_list', 'can_edit'],
-  filters: {
-    dashboard_title: [],
-    slug: [],
-    owners: [],
-    published: [],
-  },
 });
 fetchMock.get(dashboardOwnersEndpoint, {
   result: [],
@@ -86,11 +80,6 @@ describe('DashboardList', () => {
     expect(callsI).toHaveLength(1);
   });
 
-  it('fetches owners', () => {
-    const callsO = fetchMock.calls(/dashboard\/related\/owners/);
-    expect(callsO).toHaveLength(1);
-  });
-
   it('fetches data', () => {
     wrapper.update();
     const callsD = fetchMock.calls(/dashboard\/\?q/);
diff --git 
a/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx 
b/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
similarity index 94%
rename from 
superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
rename to 
superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
index 58b5ef0..9f7c285 100644
--- a/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
@@ -23,7 +23,7 @@ import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
-import DatasetList from 'src/views/datasetList/DatasetList';
+import DatasetList from 'src/views/CRUD/dataset/DatasetList';
 import ListView from 'src/components/ListView/ListView';
 import Button from 'src/components/Button';
 import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
@@ -54,13 +54,6 @@ const mockdatasets = [...new Array(3)].map((_, i) => ({
 
 fetchMock.get(datasetsInfoEndpoint, {
   permissions: ['can_list', 'can_edit', 'can_add', 'can_delete'],
-  filters: {
-    database: [],
-    schema: [],
-    table_name: [],
-    owners: [],
-    is_sqllab_view: [],
-  },
 });
 fetchMock.get(datasetsOwnersEndpoint, {
   result: [],
@@ -105,11 +98,6 @@ describe('DatasetList', () => {
     expect(callsI).toHaveLength(1);
   });
 
-  it('fetches owners', () => {
-    const callsO = fetchMock.calls(/dataset\/related\/owners/);
-    expect(callsO).toHaveLength(1);
-  });
-
   it('fetches data', () => {
     const callsD = fetchMock.calls(/dataset\/\?q/);
     expect(callsD).toHaveLength(1);
diff --git 
a/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.tsx 
b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.tsx
index 2b7ff01..029f1e0 100644
--- a/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.tsx
+++ b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.tsx
@@ -22,8 +22,8 @@ import { styled, supersetTheme } from '@superset-ui/style';
 import { t, tn } from '@superset-ui/translation';
 
 import { noOp } from 'src/utils/common';
+import Button from 'src/views/CRUD/dataset/Button';
 import Icon from '../Icon';
-import Button from '../../views/datasetList/Button';
 import { ErrorMessageComponentProps } from './types';
 import CopyToClipboard from '../CopyToClipboard';
 import IssueCode from './IssueCode';
diff --git a/superset-frontend/src/components/ListView/LegacyFilters.tsx 
b/superset-frontend/src/components/ListView/LegacyFilters.tsx
deleted file mode 100644
index 111af4b..0000000
--- a/superset-frontend/src/components/ListView/LegacyFilters.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this 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 { t } from '@superset-ui/translation';
-import React, { Dispatch, SetStateAction } from 'react';
-import {
-  Button,
-  Col,
-  DropdownButton,
-  FormControl,
-  MenuItem,
-  Row,
-} from 'react-bootstrap';
-import { Select } from 'src/components/Select';
-import { Filters, InternalFilter, SelectOption } from './types';
-import { extractInputValue, getDefaultFilterOperator } from './utils';
-
-const styleWidth100p = { width: '100%' };
-
-export const FilterMenu = ({
-  filters,
-  internalFilters,
-  setInternalFilters,
-}: {
-  filters: Filters;
-  internalFilters: InternalFilter[];
-  setInternalFilters: Dispatch<SetStateAction<InternalFilter[]>>;
-}) => (
-  <div className="filter-dropdown">
-    <DropdownButton
-      id="filter-picker"
-      bsSize="small"
-      bsStyle={'default'}
-      noCaret
-      title={
-        <>
-          <i className="fa fa-filter text-primary" />
-          {'  '}
-          {t('Filter')}
-        </>
-      }
-    >
-      {filters
-        .map(({ id, Header }) => ({
-          Header,
-          id,
-          value: undefined,
-        }))
-        .map(ft => (
-          <MenuItem
-            key={ft.id}
-            eventKey={ft}
-            // @ts-ignore
-            onSelect={(fltr: typeof ft) => {
-              setInternalFilters([...internalFilters, fltr]);
-            }}
-          >
-            {ft.Header}
-          </MenuItem>
-        ))}
-    </DropdownButton>
-  </div>
-);
-
-export const FilterInputs = ({
-  internalFilters,
-  filters,
-  updateInternalFilter,
-  removeFilterAndApply,
-  filtersApplied,
-  applyFilters,
-}: {
-  internalFilters: InternalFilter[];
-  filters: Filters;
-  updateInternalFilter: (i: number, f: object) => void;
-  removeFilterAndApply: (i: number) => void;
-  filtersApplied: boolean;
-  applyFilters: () => void;
-}) => (
-  <>
-    {internalFilters.map((ft, i) => {
-      const filter = filters.find(f => f.id === ft.id);
-      if (!filter) {
-        // eslint-disable-next-line no-console
-        console.error(`could not find filter for ${ft.id}`);
-        return null;
-      }
-      return (
-        <div key={`${ft.Header}-${i}`} className="filter-inputs">
-          <Row>
-            <Col className="text-center filter-column" md={2}>
-              <span>{ft.Header}</span>
-            </Col>
-            <Col md={2}>
-              <FormControl
-                componentClass="select"
-                bsSize="small"
-                value={ft.operator}
-                placeholder={filter ? getDefaultFilterOperator(filter) : ''}
-                // @ts-ignore
-                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                  updateInternalFilter(i, {
-                    operator: e.currentTarget.value,
-                  });
-                }}
-              >
-                {(filter.operators || []).map(
-                  ({ label, value }: SelectOption) => (
-                    <option key={label} value={value}>
-                      {label}
-                    </option>
-                  ),
-                )}
-              </FormControl>
-            </Col>
-            <Col md={1} />
-            <Col md={4}>
-              {filter.input === 'select' && (
-                <Select
-                  autoFocus
-                  multi
-                  searchable
-                  name={`filter-${filter.id}-select`}
-                  options={filter.selects}
-                  placeholder="Select Value"
-                  value={ft.value as SelectOption['value'][] | undefined}
-                  onChange={(e: SelectOption[] | null) => {
-                    updateInternalFilter(i, {
-                      operator: ft.operator || 
getDefaultFilterOperator(filter),
-                      value: e ? e.map(s => s.value) : e,
-                    });
-                  }}
-                />
-              )}
-              {filter.input !== 'select' && (
-                // @ts-ignore
-                <FormControl
-                  type={filter.input ? filter.input : 'text'}
-                  bsSize="small"
-                  value={String(ft.value || '')}
-                  checked={Boolean(ft.value)}
-                  // @ts-ignore
-                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                    e.persist();
-                    updateInternalFilter(i, {
-                      operator: ft.operator || 
getDefaultFilterOperator(filter),
-                      value: extractInputValue(filter.input, e),
-                    });
-                  }}
-                />
-              )}
-            </Col>
-            <Col md={1}>
-              <div
-                className="filter-close"
-                role="button"
-                tabIndex={0}
-                onClick={() => removeFilterAndApply(i)}
-              >
-                <i className="fa fa-close text-primary" />
-              </div>
-            </Col>
-          </Row>
-          <br />
-        </div>
-      );
-    })}
-    {internalFilters.length > 0 && (
-      <>
-        <Row>
-          <Col md={11} />
-          <Col md={1}>
-            <Button
-              data-test="apply-filters"
-              disabled={!!filtersApplied}
-              bsStyle="primary"
-              style={styleWidth100p}
-              onClick={applyFilters}
-              bsSize="small"
-            >
-              {t('Apply')}
-            </Button>
-          </Col>
-        </Row>
-        <br />
-      </>
-    )}
-  </>
-);
diff --git a/superset-frontend/src/components/ListView/ListView.tsx 
b/superset-frontend/src/components/ListView/ListView.tsx
index 7a7216f..970658a 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -26,7 +26,6 @@ import Loading from 'src/components/Loading';
 import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
 import TableCollection from './TableCollection';
 import Pagination from './Pagination';
-import { FilterMenu, FilterInputs } from './LegacyFilters';
 import FilterControls from './Filters';
 import { FetchDataConfig, Filters, SortColumn } from './types';
 import { ListViewError, useListViewState } from './utils';
@@ -198,7 +197,6 @@ export interface ListViewProps {
     onSelect: (rows: any[]) => any;
     type?: 'primary' | 'secondary' | 'danger';
   }>;
-  isSIP34FilterUIEnabled?: boolean;
   bulkSelectEnabled?: boolean;
   disableBulkSelect?: () => void;
   renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
@@ -263,7 +261,6 @@ const ListView: FunctionComponent<ListViewProps> = ({
   className = '',
   filters = [],
   bulkActions = [],
-  isSIP34FilterUIEnabled = false,
   bulkSelectEnabled = false,
   disableBulkSelect = () => {},
   renderBulkSelectCopy = selected => t('%s Selected', selected.length),
@@ -276,12 +273,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
     prepareRow,
     pageCount = 1,
     gotoPage,
-    removeFilterAndApply,
-    setInternalFilters,
-    updateInternalFilter,
     applyFilterValue,
-    applyFilters,
-    filtersApplied,
     selectedFlatRows,
     toggleAllRowsSelected,
     state: { pageIndex, pageSize, internalFilters },
@@ -294,7 +286,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
     fetchData,
     initialPageSize,
     initialSort,
-    initialFilters: isSIP34FilterUIEnabled ? filters : [],
+    initialFilters: filters,
   });
   const filterable = Boolean(filters.length);
   if (filterable) {
@@ -317,30 +309,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
     <ListViewStyles>
       <div className={`superset-list-view ${className}`}>
         <div className="header">
-          {!isSIP34FilterUIEnabled && filterable && (
-            <>
-              <Row>
-                <Col md={10} />
-                <Col md={2}>
-                  <FilterMenu
-                    filters={filters}
-                    internalFilters={internalFilters}
-                    setInternalFilters={setInternalFilters}
-                  />
-                </Col>
-              </Row>
-              <hr />
-              <FilterInputs
-                internalFilters={internalFilters}
-                filters={filters}
-                updateInternalFilter={updateInternalFilter}
-                removeFilterAndApply={removeFilterAndApply}
-                filtersApplied={filtersApplied}
-                applyFilters={applyFilters}
-              />
-            </>
-          )}
-          {isSIP34FilterUIEnabled && filterable && (
+          {filterable && (
             <FilterControls
               filters={filters}
               internalFilters={internalFilters}
diff --git a/superset-frontend/src/components/ListView/types.ts 
b/superset-frontend/src/components/ListView/types.ts
index dbb283b..8a8b4f7 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -32,7 +32,19 @@ export interface Filter {
   Header: string;
   id: string;
   operators?: SelectOption[];
-  operator?: string;
+  operator?:
+    | 'sw'
+    | 'ew'
+    | 'ct'
+    | 'eq'
+    | 'nsw'
+    | 'new'
+    | 'nct'
+    | 'neq'
+    | 'rel_m_m'
+    | 'rel_o_m'
+    | 'title_or_slug'
+    | 'name_or_description';
   input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search';
   unfilteredLabel?: string;
   selects?: SelectOption[];
@@ -63,19 +75,3 @@ export interface FetchDataConfig {
 export interface InternalFilter extends FilterValue {
   Header?: string;
 }
-
-export interface FilterOperatorMap {
-  [columnId: string]: Array<{
-    name: string;
-    operator:
-      | 'sw'
-      | 'ew'
-      | 'ct'
-      | 'eq'
-      | 'nsw'
-      | 'new'
-      | 'nct'
-      | 'neq'
-      | 'rel_m_m';
-  }>;
-}
diff --git a/superset-frontend/src/components/ListView/utils.ts 
b/superset-frontend/src/components/ListView/utils.ts
index f3a6975..b73d8ad 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -225,18 +225,6 @@ export function useListViewState({
     }
   }, [query]);
 
-  const filtersApplied = internalFilters.every(
-    ({ id, value, operator }, index) =>
-      id &&
-      filters[index]?.id === id &&
-      filters[index]?.value === value &&
-      // @ts-ignore
-      filters[index]?.operator === operator,
-  );
-
-  const updateInternalFilter = (index: number, update: object) =>
-    setInternalFilters(updateInList(internalFilters, index, update));
-
   const applyFilterValue = (index: number, value: any) => {
     // skip redunundant updates
     if (internalFilters[index].value === value) {
@@ -249,18 +237,9 @@ export function useListViewState({
     gotoPage(0); // clear pagination on filter
   };
 
-  const removeFilterAndApply = (index: number) => {
-    const updated = removeFromList(internalFilters, index);
-    setInternalFilters(updated);
-    setAllFilters(convertFilters(updated));
-  };
-
   return {
-    applyFilters: () => setAllFilters(convertFilters(internalFilters)),
-    removeFilterAndApply,
     canNextPage,
     canPreviousPage,
-    filtersApplied,
     getTableBodyProps,
     getTableProps,
     gotoPage,
@@ -270,10 +249,8 @@ export function useListViewState({
     rows,
     selectedFlatRows,
     setAllFilters,
-    setInternalFilters,
     state: { pageIndex, pageSize, sortBy, filters, internalFilters },
     toggleAllRowsSelected,
-    updateInternalFilter,
     applyFilterValue,
   };
 }
diff --git a/superset-frontend/src/components/Modal.tsx 
b/superset-frontend/src/components/Modal.tsx
index 1c67ffe..f9d2fae 100644
--- a/superset-frontend/src/components/Modal.tsx
+++ b/superset-frontend/src/components/Modal.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 import styled from '@superset-ui/style';
 import { Modal as BaseModal } from 'react-bootstrap';
 import { t } from '@superset-ui/translation';
-import Button from '../views/datasetList/Button';
+import Button from 'src/views/CRUD/dataset/Button';
 
 interface ModalProps {
   children: React.ReactNode;
diff --git a/superset-frontend/src/featureFlags.ts 
b/superset-frontend/src/featureFlags.ts
index e157603..0853578 100644
--- a/superset-frontend/src/featureFlags.ts
+++ b/superset-frontend/src/featureFlags.ts
@@ -26,7 +26,6 @@ export enum FeatureFlag {
   ESTIMATE_QUERY_COST = 'ESTIMATE_QUERY_COST',
   SHARE_QUERIES_VIA_KV_STORE = 'SHARE_QUERIES_VIA_KV_STORE',
   SQLLAB_BACKEND_PERSISTENCE = 'SQLLAB_BACKEND_PERSISTENCE',
-  LIST_VIEWS_SIP34_FILTER_UI = 'LIST_VIEWS_SIP34_FILTER_UI',
 }
 
 export type FeatureFlagMap = {
diff --git a/superset-frontend/src/views/chartList/ChartList.tsx 
b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
similarity index 68%
rename from superset-frontend/src/views/chartList/ChartList.tsx
rename to superset-frontend/src/views/CRUD/chart/ChartList.tsx
index d7a4336..30649bc 100644
--- a/superset-frontend/src/views/chartList/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -22,21 +22,15 @@ import { getChartMetadataRegistry } from 
'@superset-ui/chart';
 import PropTypes from 'prop-types';
 import React from 'react';
 import rison from 'rison';
-// @ts-ignore
-import { Panel } from 'react-bootstrap';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
 import Icon from 'src/components/Icon';
 import ListView, { ListViewProps } from 'src/components/ListView/ListView';
-import {
-  FetchDataConfig,
-  FilterOperatorMap,
-  Filters,
-} from 'src/components/ListView/types';
+import { FetchDataConfig, Filters } from 'src/components/ListView/types';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import PropertiesModal, { Slice } from 
'src/explore/components/PropertiesModal';
 import Chart from 'src/types/Chart';
-import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
 
 const PAGE_SIZE = 25;
 
@@ -49,8 +43,6 @@ interface State {
   bulkSelectEnabled: boolean;
   chartCount: number;
   charts: any[];
-  filterOperators: FilterOperatorMap;
-  filters: Filters;
   lastFetchDataConfig: FetchDataConfig | null;
   loading: boolean;
   permissions: string[];
@@ -58,7 +50,23 @@ interface State {
   // In future it would be better to have a unified Chart entity.
   sliceCurrentlyEditing: Slice | null;
 }
+const createFetchDatasets = (
+  handleError: (err: Response) => void,
+) => async () => {
+  try {
+    const { json = {} } = await SupersetClient.get({
+      endpoint: '/api/v1/chart/datasources',
+    });
 
+    return json?.result?.map((ds: { label: string; value: any }) => ({
+      ...ds,
+      value: JSON.stringify(ds.value),
+    }));
+  } catch (e) {
+    handleError(e);
+  }
+  return [];
+};
 class ChartList extends React.PureComponent<Props, State> {
   static propTypes = {
     addDangerToast: PropTypes.func.isRequired,
@@ -68,8 +76,6 @@ class ChartList extends React.PureComponent<Props, State> {
     bulkSelectEnabled: false,
     chartCount: 0,
     charts: [],
-    filterOperators: {},
-    filters: [],
     lastFetchDataConfig: null,
     loading: true,
     permissions: [],
@@ -81,20 +87,15 @@ class ChartList extends React.PureComponent<Props, State> {
       endpoint: `/api/v1/chart/_info`,
     }).then(
       ({ json: infoJson = {} }) => {
-        this.setState(
-          {
-            filterOperators: infoJson.filters,
-            permissions: infoJson.permissions,
-          },
-          this.updateFilters,
-        );
+        this.setState({
+          permissions: infoJson.permissions,
+        });
       },
-      e => {
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t('An error occurred while fetching charts: %s', e.statusText),
-        );
-        console.error(e);
-      },
+          t('An error occurred while fetching chart info: %s', errMsg),
+        ),
+      ),
     );
   }
 
@@ -106,10 +107,6 @@ class ChartList extends React.PureComponent<Props, State> {
     return this.hasPerm('can_delete');
   }
 
-  get isSIP34FilterUIEnabled() {
-    return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI);
-  }
-
   initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
   columns = [
@@ -228,6 +225,63 @@ class ChartList extends React.PureComponent<Props, State> {
     },
   ];
 
+  filters: Filters = [
+    {
+      Header: t('Owner'),
+      id: 'owners',
+      input: 'select',
+      operator: 'rel_m_m',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchRelated(
+        'chart',
+        'owners',
+        createErrorHandler(errMsg =>
+          this.props.addDangerToast(
+            t(
+              'An error occurred while fetching chart dataset values: %s',
+              errMsg,
+            ),
+          ),
+        ),
+      ),
+      paginate: true,
+    },
+    {
+      Header: t('Viz Type'),
+      id: 'viz_type',
+      input: 'select',
+      operator: 'eq',
+      unfilteredLabel: 'All',
+      selects: getChartMetadataRegistry()
+        .keys()
+        .map(k => ({ label: k, value: k })),
+    },
+    {
+      Header: t('Dataset'),
+      id: 'datasource',
+      input: 'select',
+      operator: 'eq',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchDatasets(
+        createErrorHandler(errMsg =>
+          this.props.addDangerToast(
+            t(
+              'An error occurred while fetching chart dataset values: %s',
+              errMsg,
+            ),
+          ),
+        ),
+      ),
+      paginate: false,
+    },
+    {
+      Header: t('Search'),
+      id: 'slice_name',
+      input: 'search',
+      operator: 'name_or_description',
+    },
+  ];
+
   hasPerm = (perm: string) => {
     if (!this.state.permissions.length) {
       return false;
@@ -295,15 +349,11 @@ class ChartList extends React.PureComponent<Props, State> 
{
         }
         this.props.addSuccessToast(json.message);
       },
-      (err: any) => {
-        console.error(err);
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t(
-            'There was an issue deleting the selected charts: %s',
-            err.statusText,
-          ),
-        );
-      },
+          t('There was an issue deleting the selected charts: %s', errMsg),
+        ),
+      ),
     );
   };
 
@@ -354,171 +404,27 @@ class ChartList extends React.PureComponent<Props, 
State> {
     return SupersetClient.get({
       endpoint: `/api/v1/chart/?q=${queryParams}`,
     })
-      .then(({ json = {} }) => {
-        this.setState({ charts: json.result, chartCount: json.count });
-      })
-      .catch(e => {
-        console.log(e.body);
-        this.props.addDangerToast(
-          t('An error occurred while fetching charts: %s', e.statusText),
-        );
-      })
+      .then(
+        ({ json = {} }) => {
+          this.setState({ charts: json.result, chartCount: json.count });
+        },
+        createErrorHandler(errMsg =>
+          this.props.addDangerToast(
+            t('An error occurred while fetching charts: %s', errMsg),
+          ),
+        ),
+      )
       .finally(() => {
         this.setState({ loading: false });
       });
   };
 
-  fetchOwners = async (
-    filterValue = '',
-    pageIndex?: number,
-    pageSize?: number,
-  ) => {
-    const resource = '/api/v1/chart/related/owners';
-
-    try {
-      const queryParams = rison.encode({
-        ...(pageIndex ? { page: pageIndex } : {}),
-        ...(pageSize ? { page_ize: pageSize } : {}),
-        ...(filterValue ? { filter: filterValue } : {}),
-      });
-      const { json = {} } = await SupersetClient.get({
-        endpoint: `${resource}?q=${queryParams}`,
-      });
-
-      return json?.result?.map(
-        ({ text: label, value }: { text: string; value: any }) => ({
-          label,
-          value,
-        }),
-      );
-    } catch (e) {
-      console.error(e);
-      this.props.addDangerToast(
-        t(
-          'An error occurred while fetching chart owner values: %s',
-          e.statusText,
-        ),
-      );
-    }
-    return [];
-  };
-
-  fetchDatasets = async () => {
-    const resource = '/api/v1/chart/datasources';
-    try {
-      const { json = {} } = await SupersetClient.get({
-        endpoint: `${resource}`,
-      });
-
-      return json?.result?.map((ds: { label: string; value: any }) => ({
-        ...ds,
-        value: JSON.stringify(ds.value),
-      }));
-    } catch (e) {
-      this.props.addDangerToast(
-        t(
-          'An error occurred while fetching chart dataset values: %s',
-          e.statusText,
-        ),
-      );
-    }
-    return [];
-  };
-
-  updateFilters = async () => {
-    const { filterOperators } = this.state;
-
-    if (this.isSIP34FilterUIEnabled) {
-      this.setState({
-        filters: [
-          {
-            Header: 'Owner',
-            id: 'owners',
-            input: 'select',
-            operator: 'rel_m_m',
-            unfilteredLabel: 'All',
-            fetchSelects: this.fetchOwners,
-            paginate: true,
-          },
-          {
-            Header: 'Viz Type',
-            id: 'viz_type',
-            input: 'select',
-            operator: 'eq',
-            unfilteredLabel: 'All',
-            selects: getChartMetadataRegistry()
-              .keys()
-              .map(k => ({ label: k, value: k })),
-          },
-          {
-            Header: 'Dataset',
-            id: 'datasource',
-            input: 'select',
-            operator: 'eq',
-            unfilteredLabel: 'All',
-            fetchSelects: this.fetchDatasets,
-            paginate: false,
-          },
-          {
-            Header: 'Search',
-            id: 'slice_name',
-            input: 'search',
-            operator: 'name_or_description',
-          },
-        ],
-      });
-      return;
-    }
-
-    const convertFilter = ({
-      name: label,
-      operator,
-    }: {
-      name: string;
-      operator: string;
-    }) => ({ label, value: operator });
-
-    const owners = await this.fetchOwners();
-    this.setState({
-      filters: [
-        {
-          Header: 'Chart',
-          id: 'slice_name',
-          operators: filterOperators.slice_name.map(convertFilter),
-        },
-        {
-          Header: 'Description',
-          id: 'description',
-          operators: filterOperators.slice_name.map(convertFilter),
-        },
-        {
-          Header: 'Visualization Type',
-          id: 'viz_type',
-          operators: filterOperators.viz_type.map(convertFilter),
-        },
-        {
-          Header: 'Datasource Name',
-          id: 'datasource_name',
-          operators: filterOperators.datasource_name.map(convertFilter),
-        },
-        {
-          Header: 'Owners',
-          id: 'owners',
-          input: 'select',
-          operators: filterOperators.owners.map(convertFilter),
-          selects: owners,
-        },
-      ],
-    });
-  };
-
   render() {
     const {
       bulkSelectEnabled,
       charts,
       chartCount,
       loading,
-      filters,
       sliceCurrentlyEditing,
     } = this.state;
     return (
@@ -536,9 +442,9 @@ class ChartList extends React.PureComponent<Props, State> {
         />
         {sliceCurrentlyEditing && (
           <PropertiesModal
-            show
             onHide={this.closeChartEditModal}
             onSave={this.handleChartUpdated}
+            show
             slice={sliceCurrentlyEditing}
           />
         )}
@@ -563,19 +469,18 @@ class ChartList extends React.PureComponent<Props, State> 
{
 
             return (
               <ListView
+                bulkActions={bulkActions}
+                bulkSelectEnabled={bulkSelectEnabled}
                 className="chart-list-view"
                 columns={this.columns}
-                data={charts}
                 count={chartCount}
-                pageSize={PAGE_SIZE}
+                data={charts}
+                disableBulkSelect={this.toggleBulkSelect}
                 fetchData={this.fetchData}
-                loading={loading}
+                filters={this.filters}
                 initialSort={this.initialSort}
-                filters={filters}
-                bulkActions={bulkActions}
-                bulkSelectEnabled={bulkSelectEnabled}
-                disableBulkSelect={this.toggleBulkSelect}
-                isSIP34FilterUIEnabled={this.isSIP34FilterUIEnabled}
+                loading={loading}
+                pageSize={PAGE_SIZE}
               />
             );
           }}
diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx 
b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
similarity index 73%
rename from superset-frontend/src/views/dashboardList/DashboardList.tsx
rename to superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index de23ee8..1489a58 100644
--- a/superset-frontend/src/views/dashboardList/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -21,21 +21,15 @@ import { t } from '@superset-ui/translation';
 import PropTypes from 'prop-types';
 import React from 'react';
 import rison from 'rison';
-// @ts-ignore
-import { Panel } from 'react-bootstrap';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
 import ListView, { ListViewProps } from 'src/components/ListView/ListView';
 import ExpandableList from 'src/components/ExpandableList';
-import {
-  FetchDataConfig,
-  FilterOperatorMap,
-  Filters,
-} from 'src/components/ListView/types';
+import { FetchDataConfig, Filters } from 'src/components/ListView/types';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import Icon from 'src/components/Icon';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
-import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
 
 const PAGE_SIZE = 25;
 
@@ -49,8 +43,6 @@ interface State {
   dashboardCount: number;
   dashboards: any[];
   dashboardToEdit: Dashboard | null;
-  filterOperators: FilterOperatorMap;
-  filters: Filters;
   lastFetchDataConfig: FetchDataConfig | null;
   loading: boolean;
   permissions: string[];
@@ -77,8 +69,6 @@ class DashboardList extends React.PureComponent<Props, State> 
{
     dashboardCount: 0,
     dashboards: [],
     dashboardToEdit: null,
-    filterOperators: {},
-    filters: [],
     lastFetchDataConfig: null,
     loading: true,
     permissions: [],
@@ -89,23 +79,15 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       endpoint: `/api/v1/dashboard/_info`,
     }).then(
       ({ json: infoJson = {} }) => {
-        this.setState(
-          {
-            filterOperators: infoJson.filters,
-            permissions: infoJson.permissions,
-          },
-          this.updateFilters,
-        );
+        this.setState({
+          permissions: infoJson.permissions,
+        });
       },
-      e => {
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t(
-            'An error occurred while fetching Dashboards: %s, %s',
-            e.statusText,
-          ),
-        );
-        console.error(e);
-      },
+          t('An error occurred while fetching Dashboards: %s, %s', errMsg),
+        ),
+      ),
     );
   }
 
@@ -121,10 +103,6 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     return this.hasPerm('can_mulexport');
   }
 
-  get isSIP34FilterUIEnabled() {
-    return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI);
-  }
-
   initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
   columns = [
@@ -260,6 +238,46 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     this.setState({ bulkSelectEnabled: !this.state.bulkSelectEnabled });
   };
 
+  filters: Filters = [
+    {
+      Header: 'Owner',
+      id: 'owners',
+      input: 'select',
+      operator: 'rel_m_m',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchRelated(
+        'dashboard',
+        'owners',
+        createErrorHandler(errMsg =>
+          this.props.addDangerToast(
+            t(
+              'An error occurred while fetching chart owner values: %s',
+              errMsg,
+            ),
+          ),
+        ),
+      ),
+      paginate: true,
+    },
+    {
+      Header: 'Published',
+      id: 'published',
+      input: 'select',
+      operator: 'eq',
+      unfilteredLabel: 'Any',
+      selects: [
+        { label: 'Published', value: true },
+        { label: 'Unpublished', value: false },
+      ],
+    },
+    {
+      Header: 'Search',
+      id: 'dashboard_title',
+      input: 'search',
+      operator: 'title_or_slug',
+    },
+  ];
+
   hasPerm = (perm: string) => {
     if (!this.state.permissions.length) {
       return false;
@@ -278,8 +296,8 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     this.setState({ loading: true });
     return SupersetClient.get({
       endpoint: `/api/v1/dashboard/${edits.id}`,
-    })
-      .then(({ json = {} }) => {
+    }).then(
+      ({ json = {} }) => {
         this.setState({
           dashboards: this.state.dashboards.map(dashboard => {
             if (dashboard.id === json.id) {
@@ -289,12 +307,13 @@ class DashboardList extends React.PureComponent<Props, 
State> {
           }),
           loading: false,
         });
-      })
-      .catch(e => {
+      },
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t('An error occurred while fetching dashboards: %s', e.statusText),
-        );
-      });
+          t('An error occurred while fetching dashboards: %s', errMsg),
+        ),
+      ),
+    );
   };
 
   handleDashboardDelete = ({
@@ -311,12 +330,11 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         }
         this.props.addSuccessToast(t('Deleted: %s', dashboardTitle));
       },
-      (err: any) => {
-        console.error(err);
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t('There was an issue deleting %s', dashboardTitle),
-        );
-      },
+          t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
+        ),
+      ),
     );
 
   handleBulkDashboardDelete = (dashboards: Dashboard[]) => {
@@ -332,15 +350,11 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         }
         this.props.addSuccessToast(json.message);
       },
-      (err: any) => {
-        console.error(err);
+      createErrorHandler(errMsg =>
         this.props.addDangerToast(
-          t(
-            'There was an issue deleting the selected dashboards: ',
-            err.statusText,
-          ),
-        );
-      },
+          t('There was an issue deleting the selected dashboards: ', errMsg),
+        ),
+      ),
     );
   };
 
@@ -380,137 +394,31 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     return SupersetClient.get({
       endpoint: `/api/v1/dashboard/?q=${queryParams}`,
     })
-      .then(({ json = {} }) => {
-        this.setState({ dashboards: json.result, dashboardCount: json.count });
-      })
-      .catch(e => {
-        this.props.addDangerToast(
-          t('An error occurred while fetching dashboards: %s', e.statusText),
-        );
-      })
+      .then(
+        ({ json = {} }) => {
+          this.setState({
+            dashboards: json.result,
+            dashboardCount: json.count,
+          });
+        },
+        createErrorHandler(errMsg =>
+          this.props.addDangerToast(
+            t('An error occurred while fetching dashboards: %s', errMsg),
+          ),
+        ),
+      )
       .finally(() => {
         this.setState({ loading: false });
       });
   };
 
-  fetchOwners = async (
-    filterValue = '',
-    pageIndex?: number,
-    pageSize?: number,
-  ) => {
-    const resource = '/api/v1/dashboard/related/owners';
-
-    try {
-      const queryParams = rison.encode({
-        ...(pageIndex ? { page: pageIndex } : {}),
-        ...(pageSize ? { page_ize: pageSize } : {}),
-        ...(filterValue ? { filter: filterValue } : {}),
-      });
-      const { json = {} } = await SupersetClient.get({
-        endpoint: `${resource}?q=${queryParams}`,
-      });
-
-      return json?.result?.map(
-        ({ text: label, value }: { text: string; value: any }) => ({
-          label,
-          value,
-        }),
-      );
-    } catch (e) {
-      console.error(e);
-      this.props.addDangerToast(
-        t(
-          'An error occurred while fetching chart owner values: %s',
-          e.statusText,
-        ),
-      );
-    }
-    return [];
-  };
-
-  updateFilters = async () => {
-    const { filterOperators } = this.state;
-
-    if (this.isSIP34FilterUIEnabled) {
-      return this.setState({
-        filters: [
-          {
-            Header: 'Owner',
-            id: 'owners',
-            input: 'select',
-            operator: 'rel_m_m',
-            unfilteredLabel: 'All',
-            fetchSelects: this.fetchOwners,
-            paginate: true,
-          },
-          {
-            Header: 'Published',
-            id: 'published',
-            input: 'select',
-            operator: 'eq',
-            unfilteredLabel: 'Any',
-            selects: [
-              { label: 'Published', value: true },
-              { label: 'Unpublished', value: false },
-            ],
-          },
-          {
-            Header: 'Search',
-            id: 'dashboard_title',
-            input: 'search',
-            operator: 'title_or_slug',
-          },
-        ],
-      });
-    }
-
-    const convertFilter = ({
-      name: label,
-      operator,
-    }: {
-      name: string;
-      operator: string;
-    }) => ({ label, value: operator });
-
-    const owners = await this.fetchOwners();
-
-    return this.setState({
-      filters: [
-        {
-          Header: 'Dashboard',
-          id: 'dashboard_title',
-          operators: filterOperators.dashboard_title.map(convertFilter),
-        },
-        {
-          Header: 'Slug',
-          id: 'slug',
-          operators: filterOperators.slug.map(convertFilter),
-        },
-        {
-          Header: 'Owners',
-          id: 'owners',
-          input: 'select',
-          operators: filterOperators.owners.map(convertFilter),
-          selects: owners,
-        },
-        {
-          Header: 'Published',
-          id: 'published',
-          input: 'checkbox',
-          operators: filterOperators.published.map(convertFilter),
-        },
-      ],
-    });
-  };
-
   render() {
     const {
       bulkSelectEnabled,
-      dashboardCount,
       dashboards,
-      dashboardToEdit,
-      filters,
+      dashboardCount,
       loading,
+      dashboardToEdit,
     } = this.state;
     return (
       <>
@@ -554,26 +462,25 @@ class DashboardList extends React.PureComponent<Props, 
State> {
               <>
                 {dashboardToEdit && (
                   <PropertiesModal
-                    show
                     dashboardId={dashboardToEdit.id}
-                    onHide={() => this.setState({ dashboardToEdit: null })}
                     onDashboardSave={this.handleDashboardEdit}
+                    onHide={() => this.setState({ dashboardToEdit: null })}
+                    show
                   />
                 )}
                 <ListView
+                  bulkActions={bulkActions}
+                  bulkSelectEnabled={bulkSelectEnabled}
                   className="dashboard-list-view"
                   columns={this.columns}
-                  data={dashboards}
                   count={dashboardCount}
-                  pageSize={PAGE_SIZE}
+                  data={dashboards}
+                  disableBulkSelect={this.toggleBulkSelect}
                   fetchData={this.fetchData}
-                  loading={loading}
+                  filters={this.filters}
                   initialSort={this.initialSort}
-                  filters={filters}
-                  bulkActions={bulkActions}
-                  bulkSelectEnabled={bulkSelectEnabled}
-                  disableBulkSelect={this.toggleBulkSelect}
-                  isSIP34FilterUIEnabled={this.isSIP34FilterUIEnabled}
+                  loading={loading}
+                  pageSize={PAGE_SIZE}
                 />
               </>
             );
diff --git a/superset-frontend/src/views/datasetList/AddDatasetModal.tsx 
b/superset-frontend/src/views/CRUD/dataset/AddDatasetModal.tsx
similarity index 93%
rename from superset-frontend/src/views/datasetList/AddDatasetModal.tsx
rename to superset-frontend/src/views/CRUD/dataset/AddDatasetModal.tsx
index c699520..5182cc8 100644
--- a/superset-frontend/src/views/datasetList/AddDatasetModal.tsx
+++ b/superset-frontend/src/views/CRUD/dataset/AddDatasetModal.tsx
@@ -24,7 +24,8 @@ import { t } from '@superset-ui/translation';
 import Icon from 'src/components/Icon';
 import Modal from 'src/components/Modal';
 import TableSelector from 'src/components/TableSelector';
-import withToasts from '../../messageToasts/enhancers/withToasts';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
+import { createErrorHandler } from 'src/views/CRUD/utils';
 
 type DatasetAddObject = {
   id: number;
@@ -95,10 +96,11 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = 
({
         addSuccessToast(t('The dataset has been saved'));
         onHide();
       })
-      .catch(e => {
-        addDangerToast(t('Error while saving dataset'));
-        console.error(e);
-      });
+      .catch(
+        createErrorHandler(errMsg =>
+          addDangerToast(t('Error while saving dataset: %s', errMsg)),
+        ),
+      );
   };
 
   return (
diff --git a/superset-frontend/src/views/datasetList/Button.tsx 
b/superset-frontend/src/views/CRUD/dataset/Button.tsx
similarity index 100%
rename from superset-frontend/src/views/datasetList/Button.tsx
rename to superset-frontend/src/views/CRUD/dataset/Button.tsx
diff --git a/superset-frontend/src/views/datasetList/DatasetList.tsx 
b/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
similarity index 75%
rename from superset-frontend/src/views/datasetList/DatasetList.tsx
rename to superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
index a0fcfd6..4e3dda6 100644
--- a/superset-frontend/src/views/datasetList/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
@@ -25,16 +25,13 @@ import React, {
   useState,
 } from 'react';
 import rison from 'rison';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import DeleteModal from 'src/components/DeleteModal';
 import ListView, { ListViewProps } from 'src/components/ListView/ListView';
 import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
 import AvatarIcon from 'src/components/AvatarIcon';
-import {
-  FetchDataConfig,
-  FilterOperatorMap,
-  Filters,
-} from 'src/components/ListView/types';
+import { FetchDataConfig, Filters } from 'src/components/ListView/types';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import TooltipWrapper from 'src/components/TooltipWrapper';
 import Icon from 'src/components/Icon';
@@ -69,29 +66,92 @@ interface DatasetListProps {
   addDangerToast: (msg: string) => void;
   addSuccessToast: (msg: string) => void;
 }
+interface Database {
+  allow_csv_upload: boolean;
+  allow_ctas: boolean;
+  allow_cvas: null | boolean;
+  allow_dml: boolean;
+  allow_multi_schema_metadata_fetch: boolean;
+  allow_run_async: boolean;
+  allows_cost_estimate: boolean;
+  allows_subquery: boolean;
+  allows_virtual_table_explore: boolean;
+  backend: string;
+  database_name: string;
+  explore_database_id: number;
+  expose_in_sqllab: boolean;
+  force_ctas_schema: null | boolean;
+  function_names: string[];
+  id: number;
+}
+
+const createFetchDatabases = (handleError: (err: Response) => void) => async (
+  filterValue = '',
+  pageIndex?: number,
+  pageSize?: number,
+) => {
+  try {
+    const queryParams = rison.encode({
+      columns: ['database_name', 'id'],
+      keys: ['none'],
+      ...(pageIndex ? { page: pageIndex } : {}),
+      ...(pageSize ? { page_size: pageSize } : {}),
+      ...(filterValue ? { filter: filterValue } : {}),
+    });
+    const { json = {} } = await SupersetClient.get({
+      endpoint: `/api/v1/database/?q=${queryParams}`,
+    });
+
+    return json?.result?.map(
+      ({ database_name: label, id: value }: Database) => ({
+        label,
+        value,
+      }),
+    );
+  } catch (e) {
+    handleError(e);
+  }
+  return [];
+};
 
+export const createFetchSchemas = (
+  handleError: (error: Response) => void,
+) => async (filterValue = '', pageIndex?: number, pageSize?: number) => {
+  try {
+    const queryParams = rison.encode({
+      ...(pageIndex ? { page: pageIndex } : {}),
+      ...(pageSize ? { page_size: pageSize } : {}),
+      ...(filterValue ? { filter: filterValue } : {}),
+    });
+    const { json = {} } = await SupersetClient.get({
+      endpoint: `/api/v1/database/schemas/?q=${queryParams}`,
+    });
+
+    return json?.result?.map(
+      ({ text: label, value }: { text: string; value: any }) => ({
+        label,
+        value,
+      }),
+    );
+  } catch (e) {
+    handleError(e);
+  }
+  return [];
+};
 const DatasetList: FunctionComponent<DatasetListProps> = ({
   addDangerToast,
   addSuccessToast,
 }) => {
-  const [databases, setDatabases] = useState<{ text: string; value: number 
}[]>(
-    [],
-  );
   const [datasetCount, setDatasetCount] = useState(0);
   const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
     (Dataset & { chart_count: number; dashboard_count: number }) | null
   >(null);
   const [datasets, setDatasets] = useState<any[]>([]);
-  const [currentFilters, setCurrentFilters] = useState<Filters>([]);
-  const [filterOperators, setFilterOperators] = useState<FilterOperatorMap>();
   const [
     lastFetchDataConfig,
     setLastFetchDataConfig,
   ] = useState<FetchDataConfig | null>(null);
   const [loading, setLoading] = useState(true);
-  const [currentOwners, setCurrentOwners] = useState<
-    { text: string; value: number }[]
-  >([]);
   const [permissions, setPermissions] = useState<string[]>([]);
 
   const [datasetAddModalOpen, setDatasetAddModalOpen] = useState<boolean>(
@@ -99,98 +159,85 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
   );
   const [bulkSelectEnabled, setBulkSelectEnabled] = useState<boolean>(false);
 
-  const updateFilters = () => {
-    const convertFilter = ({
-      name: label,
-      operator,
-    }: {
-      name: string;
-      operator: string;
-    }) => ({ label, value: operator });
-    if (filterOperators) {
-      setCurrentFilters([
-        {
-          Header: 'Database',
-          id: 'database',
-          input: 'select',
-          operators: filterOperators.database.map(convertFilter),
-          selects: databases.map(({ text: label, value }) => ({
-            label,
-            value,
-          })),
-        },
-        {
-          Header: 'Schema',
-          id: 'schema',
-          operators: filterOperators.schema.map(convertFilter),
-        },
-        {
-          Header: 'Table Name',
-          id: 'table_name',
-          operators: filterOperators.table_name.map(convertFilter),
-        },
-        {
-          Header: 'Owners',
-          id: 'owners',
-          input: 'select',
-          operators: filterOperators.owners.map(convertFilter),
-          selects: currentOwners.map(({ text: label, value }) => ({
-            label,
-            value,
-          })),
-        },
-        {
-          Header: 'SQL Lab View',
-          id: 'is_sqllab_view',
-          input: 'checkbox',
-          operators: filterOperators.is_sqllab_view.map(convertFilter),
-        },
-      ]);
-    }
-  };
+  const filterTypes: Filters = [
+    {
+      Header: t('Owner'),
+      id: 'owners',
+      input: 'select',
+      operator: 'rel_m_m',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchRelated(
+        'dataset',
+        'owners',
+        createErrorHandler(errMsg =>
+          t(
+            'An error occurred while fetching dataset owner values: %s',
+            errMsg,
+          ),
+        ),
+      ),
+      paginate: true,
+    },
+    {
+      Header: t('Datasource'),
+      id: 'database',
+      input: 'select',
+      operator: 'rel_o_m',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchDatabases(
+        createErrorHandler(errMsg =>
+          t('An error occurred while fetching datasource values: %s', errMsg),
+        ),
+      ),
+      paginate: true,
+    },
+    {
+      Header: t('Schema'),
+      id: 'schema',
+      input: 'select',
+      operator: 'eq',
+      unfilteredLabel: 'All',
+      fetchSelects: createFetchSchemas(errMsg =>
+        t('An error occurred while fetching schema values: %s', errMsg),
+      ),
+      paginate: true,
+    },
+    {
+      Header: t('Type'),
+      id: 'is_sqllab_view',
+      input: 'select',
+      operator: 'eq',
+      unfilteredLabel: 'All',
+      selects: [
+        { label: 'Virtual', value: true },
+        { label: 'Physical', value: false },
+      ],
+    },
+    {
+      Header: t('Search'),
+      id: 'table_name',
+      input: 'search',
+      operator: 'ct',
+    },
+  ];
 
-  const fetchDataset = () =>
-    Promise.all([
-      SupersetClient.get({
-        endpoint: `/api/v1/dataset/_info`,
-      }),
-      SupersetClient.get({
-        endpoint: `/api/v1/dataset/related/owners`,
-      }),
-      SupersetClient.get({
-        endpoint: `/api/v1/dataset/related/database`,
-      }),
-    ])
-      .then(
-        ([
-          { json: infoJson = {} },
-          { json: ownersJson = {} },
-          { json: databasesJson = {} },
-        ]) => {
-          setCurrentOwners(ownersJson.result);
-          setDatabases(databasesJson.result);
-          setPermissions(infoJson.permissions);
-          setFilterOperators(infoJson.filters);
-        },
-      )
-      .catch(([e1, e2]) => {
-        addDangerToast(t('An error occurred while fetching datasets'));
-        if (e1) {
-          console.error(e1);
-        }
-        if (e2) {
-          console.error(e2);
-        }
-      });
+  const fetchDatasetInfo = () => {
+    SupersetClient.get({
+      endpoint: `/api/v1/dataset/_info`,
+    }).then(
+      ({ json: infoJson = {} }) => {
+        setPermissions(infoJson.permissions);
+      },
+      createErrorHandler(errMsg =>
+        addDangerToast(t('An error occurred while fetching datasets', errMsg)),
+      ),
+    );
+  };
 
   useEffect(() => {
-    fetchDataset();
+    fetchDatasetInfo();
   }, []);
 
-  useEffect(() => {
-    updateFilters();
-  }, [databases, currentOwners, permissions, filterOperators]);
-
   const hasPerm = (perm: string) => {
     if (!permissions.length) {
       return false;
@@ -220,11 +267,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
           dashboard_count: json.dashboards.count,
         });
       })
-      .catch(() => {
-        addDangerToast(
-          t('An error occurred while fetching dataset related data'),
-        );
-      });
+      .catch(
+        createErrorHandler(errMsg =>
+          t(
+            'An error occurred while fetching dataset related data: %s',
+            errMsg,
+          ),
+        ),
+      );
 
   const columns = [
     {
@@ -477,15 +527,19 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
       return SupersetClient.get({
         endpoint: `/api/v1/dataset/?q=${queryParams}`,
       })
-        .then(({ json }) => {
-          setLoading(false);
-          setDatasets(json.result);
-          setDatasetCount(json.count);
-        })
-        .catch(() => {
-          addDangerToast(t('An error occurred while fetching datasets'));
-          setLoading(false);
-        });
+        .then(
+          ({ json }) => {
+            setLoading(false);
+            setDatasets(json.result);
+            setDatasetCount(json.count);
+          },
+          createErrorHandler(errMsg =>
+            addDangerToast(
+              t('An error occurred while fetching datasets: %s', errMsg),
+            ),
+          ),
+        )
+        .finally(() => setLoading(false));
     },
     [],
   );
@@ -501,10 +555,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
         setDatasetCurrentlyDeleting(null);
         addSuccessToast(t('Deleted: %s', tableName));
       },
-      (err: any) => {
-        console.error(err);
-        addDangerToast(t('There was an issue deleting %s', tableName));
-      },
+      createErrorHandler(errMsg =>
+        addDangerToast(
+          t('There was an issue deleting %s: %s', tableName, errMsg),
+        ),
+      ),
     );
   };
 
@@ -520,10 +575,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
         }
         addSuccessToast(json.message);
       },
-      (err: any) => {
-        console.error(err);
-        addDangerToast(t('There was an issue deleting the selected datasets'));
-      },
+      createErrorHandler(errMsg =>
+        addDangerToast(
+          t('There was an issue deleting the selected datasets: %s', errMsg),
+        ),
+      ),
     );
   };
 
@@ -578,9 +634,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
               count={datasetCount}
               pageSize={PAGE_SIZE}
               fetchData={fetchData}
+              filters={filterTypes}
               loading={loading}
               initialSort={initialSort}
-              filters={currentFilters}
               bulkActions={bulkActions}
               bulkSelectEnabled={bulkSelectEnabled}
               disableBulkSelect={() => setBulkSelectEnabled(false)}
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
new file mode 100644
index 0000000..1873542
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this 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 {
+  SupersetClient,
+  SupersetClientResponse,
+} from '@superset-ui/connection';
+import rison from 'rison';
+import getClientErrorObject from 'src/utils/getClientErrorObject';
+
+export const createFetchRelated = (
+  resource: string,
+  relation: string,
+  handleError: (error: Response) => void,
+) => async (filterValue = '', pageIndex?: number, pageSize?: number) => {
+  const resourceEndpoint = `/api/v1/${resource}/related/${relation}`;
+
+  try {
+    const queryParams = rison.encode({
+      ...(pageIndex ? { page: pageIndex } : {}),
+      ...(pageSize ? { page_ize: pageSize } : {}),
+      ...(filterValue ? { filter: filterValue } : {}),
+    });
+    const { json = {} } = await SupersetClient.get({
+      endpoint: `${resourceEndpoint}?q=${queryParams}`,
+    });
+
+    return json?.result?.map(
+      ({ text: label, value }: { text: string; value: any }) => ({
+        label,
+        value,
+      }),
+    );
+  } catch (e) {
+    handleError(e);
+  }
+  return [];
+};
+
+export const createErrorHandler = (
+  handleError: (errMsg?: string) => void,
+) => async (e: SupersetClientResponse | string) => {
+  const parsedError = await getClientErrorObject(e);
+  console.error(e); // eslint-disable-line no-console
+  handleError(parsedError.message);
+};
diff --git a/superset-frontend/src/welcome/App.tsx 
b/superset-frontend/src/welcome/App.tsx
index 267dd5a..4df1488 100644
--- a/superset-frontend/src/welcome/App.tsx
+++ b/superset-frontend/src/welcome/App.tsx
@@ -29,9 +29,9 @@ import { supersetTheme, ThemeProvider } from 
'@superset-ui/style';
 import ErrorBoundary from 'src/components/ErrorBoundary';
 import Menu from 'src/components/Menu/Menu';
 import FlashProvider from 'src/components/FlashProvider';
-import DashboardList from 'src/views/dashboardList/DashboardList';
-import ChartList from 'src/views/chartList/ChartList';
-import DatasetList from 'src/views/datasetList/DatasetList';
+import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
+import ChartList from 'src/views/CRUD/chart/ChartList';
+import DatasetList from 'src/views/CRUD/dataset/DatasetList';
 
 import messageToastReducer from '../messageToasts/reducers';
 import { initEnhancer } from '../reduxUtils';
diff --git a/superset/config.py b/superset/config.py
index d22fd80..2c2b180 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -307,7 +307,6 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
     "SIP_38_VIZ_REARCHITECTURE": False,
     "TAGGING_SYSTEM": False,
     "SQLLAB_BACKEND_PERSISTENCE": False,
-    "LIST_VIEWS_SIP34_FILTER_UI": False,
 }
 
 # This is merely a default.
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 0b8321e..34607a1 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -16,23 +16,33 @@
 # under the License.
 from typing import Any, Dict, List, Optional
 
-from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.api import expose, protect, rison, safe
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from sqlalchemy.exc import NoSuchTableError, SQLAlchemyError
 
-from superset import event_logger
+from superset import event_logger, security_manager
 from superset.databases.decorators import check_datasource_access
 from superset.databases.schemas import (
+    DatabaseSchemaResponseSchema,
     SelectStarResponseSchema,
     TableMetadataResponseSchema,
 )
 from superset.models.core import Database
 from superset.typing import FlaskResponse
 from superset.utils.core import error_msg_from_exception
-from superset.views.base_api import BaseSupersetModelRestApi
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
 from superset.views.database.filters import DatabaseFilter
 from superset.views.database.validators import sqlalchemy_uri_validator
 
+get_schemas_schema = {
+    "type": "object",
+    "properties": {
+        "page_size": {"type": "integer"},
+        "page": {"type": "integer"},
+        "filter": {"type": "string"},
+    },
+}
+
 
 def get_foreign_keys_metadata(
     database: Database, table_name: str, schema_name: Optional[str]
@@ -115,12 +125,13 @@ def get_table_metadata(
 class DatabaseRestApi(BaseSupersetModelRestApi):
     datamodel = SQLAInterface(Database)
 
-    include_route_methods = {"get_list", "table_metadata", "select_star"}
+    include_route_methods = {"get_list", "table_metadata", "select_star", 
"schemas"}
     class_permission_name = "DatabaseView"
     method_permission_name = {
         "get_list": "list",
         "table_metadata": "list",
         "select_star": "list",
+        "schemas": "list",
     }
     resource_name = "database"
     allow_browser_login = True
@@ -148,7 +159,11 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
     validators_columns = {"sqlalchemy_uri": sqlalchemy_uri_validator}
 
     openapi_spec_tag = "Database"
+    apispec_parameter_schemas = {
+        "get_schemas_schema": get_schemas_schema,
+    }
     openapi_spec_component_schemas = (
+        DatabaseSchemaResponseSchema,
         TableMetadataResponseSchema,
         SelectStarResponseSchema,
     )
@@ -265,3 +280,70 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
             return self.response(404, message="Table not found on the 
database")
         self.incr_stats("success", self.select_star.__name__)
         return self.response(200, result=result)
+
+    @expose("/schemas/", methods=["GET"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @rison(get_schemas_schema)
+    def schemas(self, **kwargs: Any) -> FlaskResponse:
+        """Get all schemas
+        ---
+        get:
+          parameters:
+          - in: query
+            name: q
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/get_schemas_schema'
+          responses:
+            200:
+              description: Related column data
+              content:
+                application/json:
+                  schema:
+                    $ref: "#/components/schemas/DatabaseSchemaResponseSchema"
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        args = kwargs.get("rison", {})
+        # handle pagination
+        page, page_size = self._handle_page_args(args)
+        filter_ = args.get("filter", "")
+
+        _, databases = self.datamodel.query(page=page, page_size=page_size)
+        result = []
+        count = 0
+        if databases:
+            for database in databases:
+                try:
+                    schemas = database.get_all_schema_names(
+                        cache=database.schema_cache_enabled,
+                        cache_timeout=database.schema_cache_timeout,
+                        force=False,
+                    )
+                except SQLAlchemyError:
+                    self.incr_stats("error", self.schemas.__name__)
+                    continue
+
+                schemas = security_manager.get_schemas_accessible_by_user(
+                    database, schemas
+                )
+                count += len(schemas)
+                for schema in schemas:
+                    if filter_:
+                        if schema.startswith(filter_):
+                            result.append({"text": schema, "value": schema})
+                    else:
+                        result.append({"text": schema, "value": schema})
+
+        return self.response(200, count=count, result=result)
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index 8b0a93e..db2fab6 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -77,3 +77,13 @@ class TableMetadataResponseSchema(Schema):
 
 class SelectStarResponseSchema(Schema):
     result = fields.String(description="SQL select star")
+
+
+class DatabaseSchemaObjectResponseSchema(Schema):
+    value = fields.String(description="Schema name")
+    text = fields.String(description="Schema display name")
+
+
+class DatabaseSchemaResponseSchema(Schema):
+    count = fields.Integer(description="The total number of schemas")
+    result = fields.Nested(DatabaseSchemaObjectResponseSchema)
diff --git a/tests/database_api_tests.py b/tests/database_api_tests.py
index 2cb2d43..9e869ba 100644
--- a/tests/database_api_tests.py
+++ b/tests/database_api_tests.py
@@ -17,6 +17,7 @@
 # isort:skip_file
 """Unit tests for Superset"""
 import json
+from unittest import mock
 
 import prison
 from sqlalchemy.sql import func
@@ -220,3 +221,41 @@ class TestDatabaseApi(SupersetTestCase):
         uri = 
f"api/v1/database/{example_db.id}/select_star/table_does_not_exist/"
         rv = self.client.get(uri)
         self.assertEqual(rv.status_code, 404)
+
+    def test_schemas(self):
+        self.login("admin")
+        dbs = db.session.query(Database).all()
+        schemas = []
+        for database in dbs:
+            schemas += database.get_all_schema_names()
+
+        rv = self.client.get("api/v1/database/schemas/")
+        response = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(len(schemas), response["count"])
+        self.assertEqual(schemas[0], response["result"][0]["value"])
+
+        rv = self.client.get(
+            f"api/v1/database/schemas/?q={prison.dumps({'filter': 'foo'})}"
+        )
+        response = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(0, len(response["result"]))
+
+        rv = self.client.get(
+            f"api/v1/database/schemas/?q={prison.dumps({'page': 0, 
'page_size': 25})}"
+        )
+        response = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(len(schemas), len(response["result"]))
+
+        rv = self.client.get(
+            f"api/v1/database/schemas/?q={prison.dumps({'page': 1, 
'page_size': 25})}"
+        )
+        response = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(0, len(response["result"]))
+
+    @mock.patch("superset.security_manager.get_schemas_accessible_by_user")
+    def test_schemas_no_access(self, mock_get_schemas_accessible_by_user):
+        mock_get_schemas_accessible_by_user.return_value = []
+        self.login("admin")
+        rv = self.client.get("api/v1/database/schemas/")
+        response = json.loads(rv.data.decode("utf-8"))
+        self.assertEqual(0, response["count"])

Reply via email to