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",
]