diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index 51a0ca47556e..94ee989c5c4c 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();
@@ -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();
+ } 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 2df2eb39c6ec..6b20c154944c 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 ae3bd627fd2a..98bae0ae9421 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 07c2cc6abf81..60c8ccb1f554 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 51026e64812c..86dd72990df3 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/datasetList/DatasetList_spec.jsx b/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
new file mode 100644
index 000000000000..b7b3e586b21d
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/datasetList/DatasetList_spec.jsx
@@ -0,0 +1,98 @@
+/**
+ * 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 React from 'react';
+import { mount } from 'enzyme';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+import fetchMock from 'fetch-mock';
+
+import DatasetList from 'src/views/datasetList/DatasetList';
+import ListView from 'src/components/ListView/ListView';
+
+// store needed for withToasts(datasetTable)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const datasetsInfoEndpoint = 'glob:*/api/v1/dataset/_info*';
+const datasetsOwnersEndpoint = 'glob:*/api/v1/dataset/related/owners*';
+const datasetsEndpoint = 'glob:*/api/v1/dataset/?*';
+
+const mockdatasets = [...new Array(3)].map((_, i) => ({
+ changed_by_name: 'user',
+ changed_by_url: 'changed_by_url',
+ 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(datasetsInfoEndpoint, {
+ permissions: ['can_list', 'can_edit'],
+ filters: {
+ database: [],
+ schema: [],
+ table_name: [],
+ owners: [],
+ is_sqllab_view: [],
+ },
+});
+fetchMock.get(datasetsOwnersEndpoint, {
+ result: [],
+});
+fetchMock.get(datasetsEndpoint, {
+ result: mockdatasets,
+ dataset_count: 3,
+});
+
+describe('DatasetList', () => {
+ const mockedProps = {};
+ const wrapper = mount(, {
+ context: { store },
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(DatasetList)).toHaveLength(1);
+ });
+
+ it('renders a ListView', () => {
+ expect(wrapper.find(ListView)).toHaveLength(1);
+ });
+
+ it('fetches 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(/dataset\/\?q/);
+ expect(callsD).toHaveLength(1);
+ expect(callsD[0][0]).toMatchInlineSnapshot(
+ `"/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 011fddee0a60..472a5a2a924e 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 10180f76139a..9b030699a8dd 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 c8e96a9960c9..2d11f0d14ebe 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 22bec9200875..913429f75daf 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 e624b56fe033..aff559e0b20a 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 = ({
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 0ec2b5e66e80..126a0570be5b 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 : (
{column.render('Header')}
diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts
index ca4a88deb9c4..d94703a301cc 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 7ff2710f4407..95f734a0f906 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 {
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 ec29d1ba39e7..8ade11e4e4da 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 {
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/datasetList/DatasetList.tsx b/superset-frontend/src/views/datasetList/DatasetList.tsx
new file mode 100644
index 000000000000..b88536fced58
--- /dev/null
+++ b/superset-frontend/src/views/datasetList/DatasetList.tsx
@@ -0,0 +1,448 @@
+/**
+ * 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 } from '@superset-ui/connection';
+import { t } from '@superset-ui/translation';
+import moment from 'moment';
+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 {
+ FetchDataConfig,
+ FilterOperatorMap,
+ Filters,
+} from 'src/components/ListView/types';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
+
+const PAGE_SIZE = 25;
+
+interface Props {
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+}
+
+interface State {
+ 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 Dataset {
+ changed_by_name: string;
+ changed_by_url: string;
+ changed_by: string;
+ changed_on: string;
+ databse_name: string;
+ explore_url: string;
+ id: number;
+ schema: string;
+ table_name: string;
+}
+
+class DatasetList extends React.PureComponent {
+ static propTypes = {
+ addDangerToast: PropTypes.func.isRequired,
+ };
+
+ state: State = {
+ datasetCount: 0,
+ datasets: [],
+ filterOperators: {},
+ filters: [],
+ lastFetchDataConfig: null,
+ loading: false,
+ owners: [],
+ databases: [],
+ permissions: [],
+ };
+
+ componentDidMount() {
+ 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 = {} },
+ ]) => {
+ this.setState(
+ {
+ filterOperators: infoJson.filters,
+ owners: ownersJson.result,
+ databases: databasesJson.result,
+ permissions: infoJson.permissions,
+ },
+ this.updateFilters,
+ );
+ },
+ ([e1, e2]) => {
+ this.props.addDangerToast(
+ t('An error occurred while fetching Datasets'),
+ );
+ if (e1) {
+ console.error(e1);
+ }
+ if (e2) {
+ console.error(e2);
+ }
+ },
+ );
+ }
+
+ get canEdit() {
+ return this.hasPerm('can_edit');
+ }
+
+ get canDelete() {
+ return this.hasPerm('can_delete');
+ }
+
+ get canCreate() {
+ return this.hasPerm('can_add');
+ }
+
+ initialSort = [{ id: 'changed_on', desc: true }];
+
+ columns = [
+ {
+ Cell: ({
+ row: {
+ original: { explore_url: exploreUrl, table_name: datasetTitle },
+ },
+ }: any) => {datasetTitle},
+ Header: t('Table'),
+ accessor: 'table_name',
+ },
+ {
+ Header: t('Databse'),
+ accessor: 'database_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by_name: changedByName,
+ changed_by_url: changedByUrl,
+ },
+ },
+ }: any) => {changedByName},
+ Header: t('Changed By'),
+ accessor: 'changed_by_fk',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on: changedOn },
+ },
+ }: any) => {moment(changedOn).fromNow()},
+ Header: t('Modified'),
+ accessor: 'changed_on',
+ sortable: true,
+ },
+ {
+ accessor: 'database',
+ hidden: true,
+ },
+ {
+ accessor: 'schema',
+ hidden: true,
+ },
+ {
+ accessor: 'owners',
+ hidden: true,
+ },
+ {
+ accessor: 'is_sqllab_view',
+ hidden: true,
+ },
+ {
+ Cell: ({ row: { state, original } }: any) => {
+ const handleDelete = () => this.handleDatasetDelete(original);
+ const handleEdit = () => this.handleDatasetEdit(original);
+ if (!this.canEdit && !this.canDelete) {
+ return null;
+ }
+ return (
+
+ {this.canDelete && (
+
+ {t('Are you sure you want to delete ')}{' '}
+ {original.table_name}?
+ >
+ }
+ onConfirm={handleDelete}
+ >
+ {confirmDelete => (
+
+
+
+ )}
+
+ )}
+ {this.canEdit && (
+
+
+
+ )}
+
+ );
+ },
+ Header: t('Actions'),
+ id: 'actions',
+ },
+ ];
+
+ hasPerm = (perm: string) => {
+ if (!this.state.permissions.length) {
+ return false;
+ }
+
+ return Boolean(this.state.permissions.find(p => p === perm));
+ };
+
+ handleDatasetEdit = ({ id }: { id: number }) => {
+ window.location.assign(`/tablemodelview/edit/${id}`);
+ };
+
+ handleDatasetDelete = ({ id, table_name: tableName }: Dataset) =>
+ SupersetClient.delete({
+ endpoint: `/api/v1/dataset/${id}`,
+ }).then(
+ () => {
+ const { lastFetchDataConfig } = this.state;
+ if (lastFetchDataConfig) {
+ this.fetchData(lastFetchDataConfig);
+ }
+ this.props.addSuccessToast(t('Deleted: %s', tableName));
+ },
+ (err: any) => {
+ console.error(err);
+ this.props.addDangerToast(
+ t('There was an issue deleting %s', tableName),
+ );
+ },
+ );
+
+ handleBulkDatasetDelete = (datasets: Dataset[]) => {
+ SupersetClient.delete({
+ endpoint: `/api/v1/dataset/?q=!(${datasets
+ .map(({ id }) => id)
+ .join(',')})`,
+ }).then(
+ ({ json = {} }) => {
+ const { lastFetchDataConfig } = this.state;
+ if (lastFetchDataConfig) {
+ this.fetchData(lastFetchDataConfig);
+ }
+ this.props.addSuccessToast(json.message);
+ },
+ (err: any) => {
+ console.error(err);
+ this.props.addDangerToast(
+ t('There was an issue deleting the selected datasets'),
+ );
+ },
+ );
+ };
+
+ fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
+ // set loading state, cache the last config for fetching data in this component.
+ this.setState({
+ lastFetchDataConfig: {
+ filters,
+ pageIndex,
+ pageSize,
+ sortBy,
+ },
+ loading: true,
+ });
+ const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
+ col,
+ opr,
+ value,
+ }));
+
+ const queryParams = JSON.stringify({
+ order_column: sortBy[0].id,
+ order_direction: sortBy[0].desc ? 'desc' : 'asc',
+ page: pageIndex,
+ page_size: pageSize,
+ ...(filterExps.length ? { filters: filterExps } : {}),
+ });
+
+ return SupersetClient.get({
+ endpoint: `/api/v1/dataset/?q=${queryParams}`,
+ })
+ .then(({ json = {} }) => {
+ this.setState({ datasets: json.result, datasetCount: json.count });
+ })
+ .catch(() => {
+ this.props.addDangerToast(
+ t('An error occurred while fetching Datasets'),
+ );
+ })
+ .finally(() => {
+ this.setState({ loading: false });
+ });
+ };
+
+ updateFilters = () => {
+ const { filterOperators, owners, databases } = this.state;
+ const convertFilter = ({
+ name: label,
+ operator,
+ }: {
+ name: string;
+ operator: string;
+ }) => ({ label, value: operator });
+
+ this.setState({
+ filters: [
+ {
+ 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: owners.map(({ text: label, value }) => ({ label, value })),
+ },
+ {
+ Header: 'SQL Lab View',
+ id: 'is_sqllab_view',
+ input: 'checkbox',
+ operators: filterOperators.is_sqllab_view.map(convertFilter),
+ },
+ ],
+ });
+ };
+
+ render() {
+ const { datasets, datasetCount, loading, filters } = this.state;
+
+ return (
+
+
+
+ {confirmDelete => {
+ const bulkActions = [];
+ if (this.canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: (
+ <>
+ Delete
+ >
+ ),
+ onSelect: confirmDelete,
+ });
+ }
+ return (
+ <>
+ {this.canCreate && (
+
+
+
+
+
+ )}
+
+ >
+ );
+ }}
+
+
+
+ );
+ }
+}
+
+export default withToasts(DatasetList);
diff --git a/superset-frontend/src/welcome/App.jsx b/superset-frontend/src/welcome/App.jsx
index 7abd84c5937e..4f7c92c71b07 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 = () => (
+
+
+
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index f249d842a99c..aaa4252e7f33 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -440,6 +440,18 @@ def make_sqla_column_compatible(
def __repr__(self):
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 1d7d466a1645..f9aec88666ff 100644
--- a/superset/connectors/sqla/views.py
+++ b/superset/connectors/sqla/views.py
@@ -29,7 +29,7 @@
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 @@ def refresh(self, tables):
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 64821dbfd63c..6e24ef218339 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 3f765b715403..847419162c2e 100644
--- a/tests/dataset_api_tests.py
+++ b/tests/dataset_api_tests.py
@@ -69,8 +69,12 @@ def test_get_dataset_list(self):
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",
]
|