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/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 5767fb1  [datasets] new, listview (react) (#9197)
5767fb1 is described below

commit 5767fb15cd21f4e66360361db41e72c053eac492
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Fri Mar 13 12:35:00 2020 -0700

    [datasets] new, listview (react) (#9197)
    
    * [datasets] new, react listview
    
    * add hidden columns to support filtering by columns not rendered
    
    * throw exception if config is incorrect
    
    * fix database filter
    
    * update endpoints to point to datasets; fix translation strings
    
    * move Link into src/components
    
    * add add new record button to datasets listview
---
 .../components/ListView/ListView_spec.jsx          | 195 ++++++++---------
 .../spec/javascripts/sqllab/Link_spec.jsx          |   2 +-
 .../spec/javascripts/sqllab/TableElement_spec.jsx  |   2 +-
 .../javascripts/views/chartList/ChartList_spec.jsx |  17 +-
 .../views/dashboardList/DashboardList_spec.jsx     |  16 +-
 .../DatasetList_spec.jsx}                          |  58 ++++--
 .../src/SqlLab/components/QueryTable.jsx           |   2 +-
 .../src/SqlLab/components/ShowSQL.jsx              |   2 +-
 .../src/SqlLab/components/TableElement.jsx         |   2 +-
 .../src/{SqlLab => }/components/Link.tsx           |  14 +-
 .../src/components/ListView/ListView.tsx           |  14 ++
 .../src/components/ListView/TableCollection.tsx    |   4 +-
 superset-frontend/src/components/ListView/utils.ts |   4 +
 .../src/views/chartList/ChartList.tsx              |   4 +-
 .../src/views/dashboardList/DashboardList.tsx      |   4 +-
 .../DatasetList.tsx}                               | 230 ++++++++++-----------
 superset-frontend/src/welcome/App.jsx              |   4 +
 superset/connectors/sqla/models.py                 |  12 ++
 superset/connectors/sqla/views.py                  |  10 +-
 superset/datasets/api.py                           |   8 +-
 tests/dataset_api_tests.py                         |   4 +
 21 files changed, 357 insertions(+), 251 deletions(-)

diff --git 
a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx 
b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index 51a0ca4..94ee989 100644
--- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
@@ -17,11 +17,12 @@
  * under the License.
  */
 import React from 'react';
-import { mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
 import { act } from 'react-dom/test-utils';
 import { MenuItem, Pagination } from 'react-bootstrap';
 
 import ListView from 'src/components/ListView/ListView';
+import { areArraysShallowEqual } from 'src/reduxUtils';
 
 describe('ListView', () => {
   const mockedProps = {
@@ -53,10 +54,6 @@ describe('ListView', () => {
     pageSize: 1,
     fetchData: jest.fn(() => []),
     loading: false,
-    filterTypes: {
-      id: [],
-      name: [{ name: 'sw', label: 'Starts With' }],
-    },
     bulkActions: [{ name: 'do something', onSelect: jest.fn() }],
   };
   const wrapper = mount(<ListView {...mockedProps} />);
@@ -71,15 +68,15 @@ describe('ListView', () => {
   it('calls fetchData on mount', () => {
     expect(wrapper.find(ListView)).toHaveLength(1);
     expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-                                          Array [
-                                            Object {
-                                              "filters": Array [],
-                                              "pageIndex": 0,
-                                              "pageSize": 1,
-                                              "sortBy": Array [],
-                                            },
-                                          ]
-                            `);
+                                                      Array [
+                                                        Object {
+                                                          "filters": Array [],
+                                                          "pageIndex": 0,
+                                                          "pageSize": 1,
+                                                          "sortBy": Array [],
+                                                        },
+                                                      ]
+                                    `);
   });
 
   it('calls fetchData on sort', () => {
@@ -90,20 +87,20 @@ describe('ListView', () => {
 
     expect(mockedProps.fetchData).toHaveBeenCalled();
     expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-                                          Array [
-                                            Object {
-                                              "filters": Array [],
-                                              "pageIndex": 0,
-                                              "pageSize": 1,
-                                              "sortBy": Array [
-                                                Object {
-                                                  "desc": false,
-                                                  "id": "id",
-                                                },
-                                              ],
-                                            },
-                                          ]
-                            `);
+                                                      Array [
+                                                        Object {
+                                                          "filters": Array [],
+                                                          "pageIndex": 0,
+                                                          "pageSize": 1,
+                                                          "sortBy": Array [
+                                                            Object {
+                                                              "desc": false,
+                                                              "id": "id",
+                                                            },
+                                                          ],
+                                                        },
+                                                      ]
+                                    `);
   });
 
   it('calls fetchData on filter', () => {
@@ -140,27 +137,27 @@ describe('ListView', () => {
     wrapper.update();
 
     expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-      Array [
-        Object {
-          "filters": Array [
-            Object {
-              "Header": "name",
-              "id": "name",
-              "operator": "sw",
-              "value": "foo",
-            },
-          ],
-          "pageIndex": 0,
-          "pageSize": 1,
-          "sortBy": Array [
-            Object {
-              "desc": false,
-              "id": "id",
-            },
-          ],
-        },
-      ]
-    `);
+                  Array [
+                    Object {
+                      "filters": Array [
+                        Object {
+                          "Header": "name",
+                          "id": "name",
+                          "operator": "sw",
+                          "value": "foo",
+                        },
+                      ],
+                      "pageIndex": 0,
+                      "pageSize": 1,
+                      "sortBy": Array [
+                        Object {
+                          "desc": false,
+                          "id": "id",
+                        },
+                      ],
+                    },
+                  ]
+            `);
   });
 
   it('calls fetchData on page change', () => {
@@ -170,27 +167,27 @@ describe('ListView', () => {
     wrapper.update();
 
     expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
-      Array [
-        Object {
-          "filters": Array [
-            Object {
-              "Header": "name",
-              "id": "name",
-              "operator": "sw",
-              "value": "foo",
-            },
-          ],
-          "pageIndex": 1,
-          "pageSize": 1,
-          "sortBy": Array [
-            Object {
-              "desc": false,
-              "id": "id",
-            },
-          ],
-        },
-      ]
-    `);
+                  Array [
+                    Object {
+                      "filters": Array [
+                        Object {
+                          "Header": "name",
+                          "id": "name",
+                          "operator": "sw",
+                          "value": "foo",
+                        },
+                      ],
+                      "pageIndex": 1,
+                      "pageSize": 1,
+                      "sortBy": Array [
+                        Object {
+                          "desc": false,
+                          "id": "id",
+                        },
+                      ],
+                    },
+                  ]
+            `);
   });
   it('handles bulk actions on 1 row', () => {
     act(() => {
@@ -215,15 +212,15 @@ describe('ListView', () => {
     bulkActionsProps.onSelect(bulkActionsProps.eventKey);
     expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
       .toMatchInlineSnapshot(`
-                        Array [
-                          Array [
-                            Object {
-                              "id": 1,
-                              "name": "data 1",
-                            },
-                          ],
-                        ]
-                `);
+                                    Array [
+                                      Array [
+                                        Object {
+                                          "id": 1,
+                                          "name": "data 1",
+                                        },
+                                      ],
+                                    ]
+                        `);
   });
   it('handles bulk actions on all rows', () => {
     act(() => {
@@ -248,18 +245,32 @@ describe('ListView', () => {
     bulkActionsProps.onSelect(bulkActionsProps.eventKey);
     expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
       .toMatchInlineSnapshot(`
-            Array [
-              Array [
-                Object {
-                  "id": 1,
-                  "name": "data 1",
-                },
-                Object {
-                  "id": 2,
-                  "name": "data 2",
-                },
-              ],
-            ]
-        `);
+                        Array [
+                          Array [
+                            Object {
+                              "id": 1,
+                              "name": "data 1",
+                            },
+                            Object {
+                              "id": 2,
+                              "name": "data 2",
+                            },
+                          ],
+                        ]
+                `);
+  });
+  it('Throws an exception if filter missing in columns', () => {
+    expect.assertions(1);
+    const props = {
+      ...mockedProps,
+      filters: [...mockedProps.filters, { id: 'some_column' }],
+    };
+    try {
+      shallow(<ListView {...props} />);
+    } catch (e) {
+      expect(e).toMatchInlineSnapshot(
+        `[ListViewError: Invalid filter config, some_column is not present in 
columns]`,
+      );
+    }
   });
 });
diff --git a/superset-frontend/spec/javascripts/sqllab/Link_spec.jsx 
b/superset-frontend/spec/javascripts/sqllab/Link_spec.jsx
index 2df2eb3..6b20c15 100644
--- a/superset-frontend/spec/javascripts/sqllab/Link_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/Link_spec.jsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 
-import Link from '../../../src/SqlLab/components/Link';
+import Link from '../../../src/components/Link';
 
 describe('Link', () => {
   const mockedProps = {
diff --git a/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx 
b/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
index ae3bd62..98bae0a 100644
--- a/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { mount, shallow } from 'enzyme';
 
-import Link from '../../../src/SqlLab/components/Link';
+import Link from '../../../src/components/Link';
 import TableElement from '../../../src/SqlLab/components/TableElement';
 import ColumnElement from '../../../src/SqlLab/components/ColumnElement';
 import { mockedActions, table } from './fixtures';
diff --git 
a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx 
b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
index 07c2cc6..60c8ccb 100644
--- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx
@@ -30,6 +30,7 @@ const mockStore = configureStore([thunk]);
 const store = mockStore({});
 
 const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
+const chartssOwnersEndpoint = 'glob:*/api/v1/chart/related/owners*';
 const chartsEndpoint = 'glob:*/api/v1/chart/?*';
 
 const mockCharts = [...new Array(3)].map((_, i) => ({
@@ -43,7 +44,16 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
 
 fetchMock.get(chartsInfoEndpoint, {
   permissions: ['can_list', 'can_edit'],
-  filters: [],
+  filters: {
+    slice_name: [],
+    description: [],
+    viz_type: [],
+    datasource_name: [],
+    owners: [],
+  },
+});
+fetchMock.get(chartssOwnersEndpoint, {
+  result: [],
 });
 fetchMock.get(chartsEndpoint, {
   result: mockCharts,
@@ -69,6 +79,11 @@ 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/dashboardList/DashboardList_spec.jsx
index 51026e6..86dd729 100644
--- 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
+++ 
b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
@@ -30,6 +30,7 @@ const mockStore = configureStore([thunk]);
 const store = mockStore({});
 
 const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
+const dashboardOwnersEndpoint = 'glob:*/api/v1/dashboard/related/owners*';
 const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
 
 const mockDashboards = [...new Array(3)].map((_, i) => ({
@@ -45,7 +46,15 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
 
 fetchMock.get(dashboardsInfoEndpoint, {
   permissions: ['can_list', 'can_edit'],
-  filters: [],
+  filters: {
+    dashboard_title: [],
+    slug: [],
+    owners: [],
+    published: [],
+  },
+});
+fetchMock.get(dashboardOwnersEndpoint, {
+  result: [],
 });
 fetchMock.get(dashboardsEndpoint, {
   result: mockDashboards,
@@ -71,6 +80,11 @@ 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/dashboardList/DashboardList_spec.jsx 
b/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
similarity index 56%
copy from 
superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
copy to 
superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
index 51026e6..b7b3e58 100644
--- 
a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
@@ -22,44 +22,55 @@ import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 
-import DashboardList from 'src/views/dashboardList/DashboardList';
+import DatasetList from 'src/views/datasetList/DatasetList';
 import ListView from 'src/components/ListView/ListView';
 
-// store needed for withToasts(DashboardTable)
+// store needed for withToasts(datasetTable)
 const mockStore = configureStore([thunk]);
 const store = mockStore({});
 
-const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
-const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
+const datasetsInfoEndpoint = 'glob:*/api/v1/dataset/_info*';
+const datasetsOwnersEndpoint = 'glob:*/api/v1/dataset/related/owners*';
+const datasetsEndpoint = 'glob:*/api/v1/dataset/?*';
 
-const mockDashboards = [...new Array(3)].map((_, i) => ({
-  id: i,
-  url: 'url',
-  dashboard_title: `title ${i}`,
+const mockdatasets = [...new Array(3)].map((_, i) => ({
   changed_by_name: 'user',
   changed_by_url: 'changed_by_url',
-  changed_by_fk: 1,
-  published: true,
+  changed_by: 'user',
   changed_on: new Date().toISOString(),
+  database_name: `db ${i}`,
+  explore_url: `/explore/table/${i}`,
+  id: i,
+  schema: `schema ${i}`,
+  table_name: `coolest table ${i}`,
 }));
 
-fetchMock.get(dashboardsInfoEndpoint, {
+fetchMock.get(datasetsInfoEndpoint, {
   permissions: ['can_list', 'can_edit'],
-  filters: [],
+  filters: {
+    database: [],
+    schema: [],
+    table_name: [],
+    owners: [],
+    is_sqllab_view: [],
+  },
+});
+fetchMock.get(datasetsOwnersEndpoint, {
+  result: [],
 });
-fetchMock.get(dashboardsEndpoint, {
-  result: mockDashboards,
-  dashboard_count: 3,
+fetchMock.get(datasetsEndpoint, {
+  result: mockdatasets,
+  dataset_count: 3,
 });
 
-describe('DashboardList', () => {
+describe('DatasetList', () => {
   const mockedProps = {};
-  const wrapper = mount(<DashboardList {...mockedProps} />, {
+  const wrapper = mount(<DatasetList {...mockedProps} />, {
     context: { store },
   });
 
   it('renders', () => {
-    expect(wrapper.find(DashboardList)).toHaveLength(1);
+    expect(wrapper.find(DatasetList)).toHaveLength(1);
   });
 
   it('renders a ListView', () => {
@@ -67,16 +78,21 @@ describe('DashboardList', () => {
   });
 
   it('fetches info', () => {
-    const callsI = fetchMock.calls(/dashboard\/_info/);
+    const callsI = fetchMock.calls(/dataset\/_info/);
     expect(callsI).toHaveLength(1);
   });
 
+  it('fetches owners', () => {
+    const callsO = fetchMock.calls(/dataset\/related\/owners/);
+    expect(callsO).toHaveLength(1);
+  });
+
   it('fetches data', () => {
     wrapper.update();
-    const callsD = fetchMock.calls(/dashboard\/\?q/);
+    const callsD = fetchMock.calls(/dataset\/\?q/);
     expect(callsD).toHaveLength(1);
     expect(callsD[0][0]).toMatchInlineSnapshot(
-      
`"/http//localhost/api/v1/dashboard/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`,
+      
`"/http//localhost/api/v1/dataset/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`,
     );
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/QueryTable.jsx 
b/superset-frontend/src/SqlLab/components/QueryTable.jsx
index 011fdde..472a5a2 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable.jsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable.jsx
@@ -23,7 +23,7 @@ import { Table } from 'reactable-arc';
 import { Label, ProgressBar, Well } from 'react-bootstrap';
 import { t } from '@superset-ui/translation';
 
-import Link from './Link';
+import Link from '../../components/Link';
 import ResultSet from './ResultSet';
 import ModalTrigger from '../../components/ModalTrigger';
 import HighlightedSql from './HighlightedSql';
diff --git a/superset-frontend/src/SqlLab/components/ShowSQL.jsx 
b/superset-frontend/src/SqlLab/components/ShowSQL.jsx
index 10180f7..9b03069 100644
--- a/superset-frontend/src/SqlLab/components/ShowSQL.jsx
+++ b/superset-frontend/src/SqlLab/components/ShowSQL.jsx
@@ -26,7 +26,7 @@ import github from 
'react-syntax-highlighter/dist/styles/hljs/github';
 
 import { t } from '@superset-ui/translation';
 
-import Link from './Link';
+import Link from '../../components/Link';
 import ModalTrigger from '../../components/ModalTrigger';
 
 registerLanguage('sql', sql);
diff --git a/superset-frontend/src/SqlLab/components/TableElement.jsx 
b/superset-frontend/src/SqlLab/components/TableElement.jsx
index c8e96a9..2d11f0d 100644
--- a/superset-frontend/src/SqlLab/components/TableElement.jsx
+++ b/superset-frontend/src/SqlLab/components/TableElement.jsx
@@ -23,7 +23,7 @@ import shortid from 'shortid';
 import { t } from '@superset-ui/translation';
 
 import CopyToClipboard from '../../components/CopyToClipboard';
-import Link from './Link';
+import Link from '../../components/Link';
 import ColumnElement from './ColumnElement';
 import ShowSQL from './ShowSQL';
 import ModalTrigger from '../../components/ModalTrigger';
diff --git a/superset-frontend/src/SqlLab/components/Link.tsx 
b/superset-frontend/src/components/Link.tsx
similarity index 91%
rename from superset-frontend/src/SqlLab/components/Link.tsx
rename to superset-frontend/src/components/Link.tsx
index 22bec92..913429f 100644
--- a/superset-frontend/src/SqlLab/components/Link.tsx
+++ b/superset-frontend/src/components/Link.tsx
@@ -21,13 +21,13 @@ import React, { ReactNode } from 'react';
 import { OverlayTrigger, Tooltip } from 'react-bootstrap';
 
 interface Props {
-  children: ReactNode;
-  className: string;
-  href: string;
-  onClick: () => void;
-  placement: string;
-  style: object;
-  tooltip: string | null;
+  children?: ReactNode;
+  className?: string;
+  href?: string;
+  onClick?: () => void;
+  placement?: string;
+  style?: object;
+  tooltip?: string | null;
 }
 
 const Link = ({
diff --git a/superset-frontend/src/components/ListView/ListView.tsx 
b/superset-frontend/src/components/ListView/ListView.tsx
index e624b56..aff559e 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -45,6 +45,7 @@ import {
 import {
   convertFilters,
   extractInputValue,
+  ListViewError,
   removeFromList,
   useListViewState,
 } from './utils';
@@ -122,6 +123,19 @@ const ListView: FunctionComponent<Props> = ({
     initialSort,
   });
   const filterable = Boolean(filters.length);
+  if (filterable) {
+    const columnAccessors = columns.reduce(
+      (acc, col) => ({ ...acc, [col.accessor || col.id]: true }),
+      {},
+    );
+    filters.forEach(f => {
+      if (!columnAccessors[f.id]) {
+        throw new ListViewError(
+          `Invalid filter config, ${f.id} is not present in columns`,
+        );
+      }
+    });
+  }
 
   const removeFilterAndApply = (index: number) => {
     const updated = removeFromList(internalFilters, index);
diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx 
b/superset-frontend/src/components/ListView/TableCollection.tsx
index 0ec2b5e..126a057 100644
--- a/superset-frontend/src/components/ListView/TableCollection.tsx
+++ b/superset-frontend/src/components/ListView/TableCollection.tsx
@@ -44,7 +44,9 @@ export default function TableCollection({
             {headerGroup.headers.map(column =>
               column.hidden ? null : (
                 <th
-                  {...column.getHeaderProps(column.getSortByToggleProps())}
+                  {...column.getHeaderProps(
+                    column.sortable ? column.getSortByToggleProps() : {},
+                  )}
                   data-test="sort-header"
                 >
                   {column.render('Header')}
diff --git a/superset-frontend/src/components/ListView/utils.ts 
b/superset-frontend/src/components/ListView/utils.ts
index ca4a88d..d94703a 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -35,6 +35,10 @@ import {
 
 import { FetchDataConfig, InternalFilter, SortColumn } from './types';
 
+export class ListViewError extends Error {
+  name = 'ListViewError';
+}
+
 // removes element from a list, returns new list
 export function removeFromList(list: any[], index: number): any[] {
   return list.filter((_, i) => index !== i);
diff --git a/superset-frontend/src/views/chartList/ChartList.tsx 
b/superset-frontend/src/views/chartList/ChartList.tsx
index 7ff2710..95f734a 100644
--- a/superset-frontend/src/views/chartList/ChartList.tsx
+++ b/superset-frontend/src/views/chartList/ChartList.tsx
@@ -252,11 +252,11 @@ class ChartList extends React.PureComponent<Props, State> 
{
         if (lastFetchDataConfig) {
           this.fetchData(lastFetchDataConfig);
         }
-        this.props.addSuccessToast(t('Deleted: %(slice_name)', sliceName));
+        this.props.addSuccessToast(t('Deleted: %s', sliceName));
       },
       () => {
         this.props.addDangerToast(
-          t('There was an issue deleting: %(slice_name)', sliceName),
+          t('There was an issue deleting: %s', sliceName),
         );
       },
     );
diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx 
b/superset-frontend/src/views/dashboardList/DashboardList.tsx
index ec29d1b..8ade11e 100644
--- a/superset-frontend/src/views/dashboardList/DashboardList.tsx
+++ b/superset-frontend/src/views/dashboardList/DashboardList.tsx
@@ -267,12 +267,12 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         if (lastFetchDataConfig) {
           this.fetchData(lastFetchDataConfig);
         }
-        this.props.addSuccessToast(`${t('Deleted')} ${dashboardTitle}`);
+        this.props.addSuccessToast(t('Deleted: %s', dashboardTitle));
       },
       (err: any) => {
         console.error(err);
         this.props.addDangerToast(
-          `${t('There was an issue deleting')}${dashboardTitle}`,
+          t('There was an issue deleting %s', dashboardTitle),
         );
       },
     );
diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx 
b/superset-frontend/src/views/datasetList/DatasetList.tsx
similarity index 65%
copy from superset-frontend/src/views/dashboardList/DashboardList.tsx
copy to superset-frontend/src/views/datasetList/DatasetList.tsx
index ec29d1b..b88536f 100644
--- a/superset-frontend/src/views/dashboardList/DashboardList.tsx
+++ b/superset-frontend/src/views/datasetList/DatasetList.tsx
@@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 // @ts-ignore
 import { Panel } from 'react-bootstrap';
+import Link from 'src/components/Link';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import ListView from 'src/components/ListView/ListView';
 import {
@@ -40,57 +41,68 @@ interface Props {
 }
 
 interface State {
-  dashboards: any[];
-  dashboardCount: number;
+  datasets: any[];
+  datasetCount: number;
   loading: boolean;
   filterOperators: FilterOperatorMap;
   filters: Filters;
   owners: Array<{ text: string; value: number }>;
+  databases: Array<{ text: string; value: number }>;
   permissions: string[];
   lastFetchDataConfig: FetchDataConfig | null;
 }
 
-interface Dashboard {
-  id: number;
-  changed_by: string;
+interface Dataset {
   changed_by_name: string;
   changed_by_url: string;
+  changed_by: string;
   changed_on: string;
-  dashboard_title: string;
-  published: boolean;
-  url: string;
+  databse_name: string;
+  explore_url: string;
+  id: number;
+  schema: string;
+  table_name: string;
 }
 
-class DashboardList extends React.PureComponent<Props, State> {
+class DatasetList extends React.PureComponent<Props, State> {
   static propTypes = {
     addDangerToast: PropTypes.func.isRequired,
   };
 
   state: State = {
-    dashboardCount: 0,
-    dashboards: [],
+    datasetCount: 0,
+    datasets: [],
     filterOperators: {},
     filters: [],
     lastFetchDataConfig: null,
     loading: false,
     owners: [],
+    databases: [],
     permissions: [],
   };
 
   componentDidMount() {
     Promise.all([
       SupersetClient.get({
-        endpoint: `/api/v1/dashboard/_info`,
+        endpoint: `/api/v1/dataset/_info`,
+      }),
+      SupersetClient.get({
+        endpoint: `/api/v1/dataset/related/owners`,
       }),
       SupersetClient.get({
-        endpoint: `/api/v1/dashboard/related/owners`,
+        endpoint: `/api/v1/dataset/related/database`,
       }),
     ]).then(
-      ([{ json: infoJson = {} }, { json: ownersJson = {} }]) => {
+      ([
+        { json: infoJson = {} },
+        { json: ownersJson = {} },
+        { json: databasesJson = {} },
+      ]) => {
         this.setState(
           {
             filterOperators: infoJson.filters,
             owners: ownersJson.result,
+            databases: databasesJson.result,
             permissions: infoJson.permissions,
           },
           this.updateFilters,
@@ -98,7 +110,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       },
       ([e1, e2]) => {
         this.props.addDangerToast(
-          t('An error occurred while fetching Dashboards'),
+          t('An error occurred while fetching Datasets'),
         );
         if (e1) {
           console.error(e1);
@@ -118,8 +130,8 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     return this.hasPerm('can_delete');
   }
 
-  get canExport() {
-    return this.hasPerm('can_mulexport');
+  get canCreate() {
+    return this.hasPerm('can_add');
   }
 
   initialSort = [{ id: 'changed_on', desc: true }];
@@ -128,12 +140,15 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     {
       Cell: ({
         row: {
-          original: { url, dashboard_title: dashboardTitle },
+          original: { explore_url: exploreUrl, table_name: datasetTitle },
         },
-      }: any) => <a href={url}>{dashboardTitle}</a>,
-      Header: t('Title'),
-      accessor: 'dashboard_title',
-      sortable: true,
+      }: any) => <a href={exploreUrl}>{datasetTitle}</a>,
+      Header: t('Table'),
+      accessor: 'table_name',
+    },
+    {
+      Header: t('Databse'),
+      accessor: 'database_name',
     },
     {
       Cell: ({
@@ -144,23 +159,8 @@ class DashboardList extends React.PureComponent<Props, 
State> {
           },
         },
       }: any) => <a href={changedByUrl}>{changedByName}</a>,
-      Header: t('Creator'),
+      Header: t('Changed By'),
       accessor: 'changed_by_fk',
-      sortable: true,
-    },
-    {
-      Cell: ({
-        row: {
-          original: { published },
-        },
-      }: any) => (
-        <span className="no-wrap">
-          {published ? <i className="fa fa-check" /> : ''}
-        </span>
-      ),
-      Header: t('Published'),
-      accessor: 'published',
-      sortable: true,
     },
     {
       Cell: ({
@@ -173,7 +173,11 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       sortable: true,
     },
     {
-      accessor: 'slug',
+      accessor: 'database',
+      hidden: true,
+    },
+    {
+      accessor: 'schema',
       hidden: true,
     },
     {
@@ -181,11 +185,14 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       hidden: true,
     },
     {
+      accessor: 'is_sqllab_view',
+      hidden: true,
+    },
+    {
       Cell: ({ row: { state, original } }: any) => {
-        const handleDelete = () => this.handleDashboardDelete(original);
-        const handleEdit = () => this.handleDashboardEdit(original);
-        const handleExport = () => this.handleBulkDashboardExport([original]);
-        if (!this.canEdit && !this.canDelete && !this.canExport) {
+        const handleDelete = () => this.handleDatasetDelete(original);
+        const handleEdit = () => this.handleDatasetEdit(original);
+        if (!this.canEdit && !this.canDelete) {
           return null;
         }
         return (
@@ -197,8 +204,8 @@ class DashboardList extends React.PureComponent<Props, 
State> {
                 title={t('Please Confirm')}
                 description={
                   <>
-                    {t('Are you sure you want to delete')}{' '}
-                    <b>{original.dashboard_title}</b>?
+                    {t('Are you sure you want to delete ')}{' '}
+                    <b>{original.table_name}</b>?
                   </>
                 }
                 onConfirm={handleDelete}
@@ -215,16 +222,6 @@ class DashboardList extends React.PureComponent<Props, 
State> {
                 )}
               </ConfirmStatusChange>
             )}
-            {this.canExport && (
-              <span
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                onClick={handleExport}
-              >
-                <i className="fa fa-database" />
-              </span>
-            )}
             {this.canEdit && (
               <span
                 role="button"
@@ -251,35 +248,32 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     return Boolean(this.state.permissions.find(p => p === perm));
   };
 
-  handleDashboardEdit = ({ id }: { id: number }) => {
-    window.location.assign(`/dashboard/edit/${id}`);
+  handleDatasetEdit = ({ id }: { id: number }) => {
+    window.location.assign(`/tablemodelview/edit/${id}`);
   };
 
-  handleDashboardDelete = ({
-    id,
-    dashboard_title: dashboardTitle,
-  }: Dashboard) =>
+  handleDatasetDelete = ({ id, table_name: tableName }: Dataset) =>
     SupersetClient.delete({
-      endpoint: `/api/v1/dashboard/${id}`,
+      endpoint: `/api/v1/dataset/${id}`,
     }).then(
       () => {
         const { lastFetchDataConfig } = this.state;
         if (lastFetchDataConfig) {
           this.fetchData(lastFetchDataConfig);
         }
-        this.props.addSuccessToast(`${t('Deleted')} ${dashboardTitle}`);
+        this.props.addSuccessToast(t('Deleted: %s', tableName));
       },
       (err: any) => {
         console.error(err);
         this.props.addDangerToast(
-          `${t('There was an issue deleting')}${dashboardTitle}`,
+          t('There was an issue deleting %s', tableName),
         );
       },
     );
 
-  handleBulkDashboardDelete = (dashboards: Dashboard[]) => {
+  handleBulkDatasetDelete = (datasets: Dataset[]) => {
     SupersetClient.delete({
-      endpoint: `/api/v1/dashboard/?q=!(${dashboards
+      endpoint: `/api/v1/dataset/?q=!(${datasets
         .map(({ id }) => id)
         .join(',')})`,
     }).then(
@@ -293,20 +287,12 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       (err: any) => {
         console.error(err);
         this.props.addDangerToast(
-          t('There was an issue deleting the selected dashboards'),
+          t('There was an issue deleting the selected datasets'),
         );
       },
     );
   };
 
-  handleBulkDashboardExport = (dashboards: Dashboard[]) => {
-    return window.location.assign(
-      `/api/v1/dashboard/export/?q=!(${dashboards
-        .map(({ id }) => id)
-        .join(',')})`,
-    );
-  };
-
   fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
     // set loading state, cache the last config for fetching data in this 
component.
     this.setState({
@@ -333,14 +319,14 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     });
 
     return SupersetClient.get({
-      endpoint: `/api/v1/dashboard/?q=${queryParams}`,
+      endpoint: `/api/v1/dataset/?q=${queryParams}`,
     })
       .then(({ json = {} }) => {
-        this.setState({ dashboards: json.result, dashboardCount: json.count });
+        this.setState({ datasets: json.result, datasetCount: json.count });
       })
       .catch(() => {
         this.props.addDangerToast(
-          t('An error occurred while fetching Dashboards'),
+          t('An error occurred while fetching Datasets'),
         );
       })
       .finally(() => {
@@ -349,7 +335,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
   };
 
   updateFilters = () => {
-    const { filterOperators, owners } = this.state;
+    const { filterOperators, owners, databases } = this.state;
     const convertFilter = ({
       name: label,
       operator,
@@ -361,14 +347,24 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     this.setState({
       filters: [
         {
-          Header: 'Dashboard',
-          id: 'dashboard_title',
-          operators: filterOperators.dashboard_title.map(convertFilter),
+          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: 'Slug',
-          id: 'slug',
-          operators: filterOperators.slug.map(convertFilter),
+          Header: 'Table Name',
+          id: 'table_name',
+          operators: filterOperators.table_name.map(convertFilter),
         },
         {
           Header: 'Owners',
@@ -378,17 +374,17 @@ class DashboardList extends React.PureComponent<Props, 
State> {
           selects: owners.map(({ text: label, value }) => ({ label, value })),
         },
         {
-          Header: 'Published',
-          id: 'published',
+          Header: 'SQL Lab View',
+          id: 'is_sqllab_view',
           input: 'checkbox',
-          operators: filterOperators.published.map(convertFilter),
+          operators: filterOperators.is_sqllab_view.map(convertFilter),
         },
       ],
     });
   };
 
   render() {
-    const { dashboards, dashboardCount, loading, filters } = this.state;
+    const { datasets, datasetCount, loading, filters } = this.state;
 
     return (
       <div className="container welcome">
@@ -396,9 +392,9 @@ class DashboardList extends React.PureComponent<Props, 
State> {
           <ConfirmStatusChange
             title={t('Please confirm')}
             description={t(
-              'Are you sure you want to delete the selected dashboards?',
+              'Are you sure you want to delete the selected datasets?',
             )}
-            onConfirm={this.handleBulkDashboardDelete}
+            onConfirm={this.handleBulkDatasetDelete}
           >
             {confirmDelete => {
               const bulkActions = [];
@@ -413,31 +409,33 @@ class DashboardList extends React.PureComponent<Props, 
State> {
                   onSelect: confirmDelete,
                 });
               }
-              if (this.canExport) {
-                bulkActions.push({
-                  key: 'export',
-                  name: (
-                    <>
-                      <i className="fa fa-database" /> Export
-                    </>
-                  ),
-                  onSelect: this.handleBulkDashboardExport,
-                });
-              }
               return (
-                <ListView
-                  className="dashboard-list-view"
-                  title={'Dashboards'}
-                  columns={this.columns}
-                  data={dashboards}
-                  count={dashboardCount}
-                  pageSize={PAGE_SIZE}
-                  fetchData={this.fetchData}
-                  loading={loading}
-                  initialSort={this.initialSort}
-                  filters={filters}
-                  bulkActions={bulkActions}
-                />
+                <>
+                  {this.canCreate && (
+                    <span className="list-add-action">
+                      <Link
+                        className="btn btn-sm btn-primary pull-right"
+                        href="/tablemodelview/add"
+                        tooltip="Add a new record"
+                      >
+                        <i className="fa fa-plus" />
+                      </Link>
+                    </span>
+                  )}
+                  <ListView
+                    className="dataset-list-view"
+                    title={'Datasets'}
+                    columns={this.columns}
+                    data={datasets}
+                    count={datasetCount}
+                    pageSize={PAGE_SIZE}
+                    fetchData={this.fetchData}
+                    loading={loading}
+                    initialSort={this.initialSort}
+                    filters={filters}
+                    bulkActions={bulkActions}
+                  />
+                </>
               );
             }}
           </ConfirmStatusChange>
@@ -447,4 +445,4 @@ class DashboardList extends React.PureComponent<Props, 
State> {
   }
 }
 
-export default withToasts(DashboardList);
+export default withToasts(DatasetList);
diff --git a/superset-frontend/src/welcome/App.jsx 
b/superset-frontend/src/welcome/App.jsx
index 7abd84c..4f7c92c 100644
--- a/superset-frontend/src/welcome/App.jsx
+++ b/superset-frontend/src/welcome/App.jsx
@@ -26,6 +26,7 @@ import { BrowserRouter as Router, Switch, Route } from 
'react-router-dom';
 import Menu from 'src/components/Menu/Menu';
 import DashboardList from 'src/views/dashboardList/DashboardList';
 import ChartList from 'src/views/chartList/ChartList';
+import DatasetList from 'src/views/datasetList/DatasetList';
 
 import messageToastReducer from '../messageToasts/reducers';
 import { initEnhancer } from '../reduxUtils';
@@ -62,6 +63,9 @@ const App = () => (
         <Route path="/chart/list/">
           <ChartList user={user} />
         </Route>
+        <Route path="/tablemodelview/list/">
+          <DatasetList user={user} />
+        </Route>
       </Switch>
       <ToastPresenter />
     </Router>
diff --git a/superset/connectors/sqla/models.py 
b/superset/connectors/sqla/models.py
index f249d84..aaa4252 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -441,6 +441,18 @@ class SqlaTable(Model, BaseDatasource):
         return self.name
 
     @property
+    def changed_by_name(self) -> str:
+        if not self.changed_by:
+            return ""
+        return str(self.changed_by)
+
+    @property
+    def changed_by_url(self) -> str:
+        if not self.changed_by:
+            return ""
+        return f"/superset/profile/{self.changed_by.username}"
+
+    @property
     def connection(self) -> str:
         return str(self.database)
 
diff --git a/superset/connectors/sqla/views.py 
b/superset/connectors/sqla/views.py
index 1d7d466..f9aec88 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -29,7 +29,7 @@ from flask_babel import gettext as __, lazy_gettext as _
 from wtforms.ext.sqlalchemy.fields import QuerySelectField
 from wtforms.validators import Regexp
 
-from superset import appbuilder, db, security_manager
+from superset import app, appbuilder, db, security_manager
 from superset.connectors.base.views import DatasourceModelView
 from superset.constants import RouteMethod
 from superset.utils import core as utils
@@ -461,3 +461,11 @@ class TableModelView(DatasourceModelView, DeleteMixin, 
YamlExportMixin):
             flash(failure_msg, "danger")
 
         return redirect("/tablemodelview/list/")
+
+    @expose("/list/")
+    @has_access
+    def list(self):
+        if not app.config["ENABLE_REACT_CRUD_VIEWS"]:
+            return super().list()
+
+        return super().render_app_template()
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index f0f4b7f..2e12629 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -52,11 +52,15 @@ class DatasetRestApi(BaseSupersetModelRestApi):
     include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | 
{RouteMethod.RELATED}
 
     list_columns = [
-        "database_name",
+        "changed_by_name",
+        "changed_by_url",
         "changed_by.username",
         "changed_on",
-        "table_name",
+        "database_name",
+        "explore_url",
+        "id",
         "schema",
+        "table_name",
     ]
     show_columns = [
         "database.database_name",
diff --git a/tests/dataset_api_tests.py b/tests/dataset_api_tests.py
index 3f765b7..8474191 100644
--- a/tests/dataset_api_tests.py
+++ b/tests/dataset_api_tests.py
@@ -69,8 +69,12 @@ class DatasetApiTests(SupersetTestCase):
         self.assertEqual(response["count"], 1)
         expected_columns = [
             "changed_by",
+            "changed_by_name",
+            "changed_by_url",
             "changed_on",
             "database_name",
+            "explore_url",
+            "id",
             "schema",
             "table_name",
         ]

Reply via email to