diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 6d40634da..ab4586f4b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -4912,6 +4912,7 @@ "version": "0.6.11", "resolved": "https://registry.npmjs.org/@types/react-json-tree/-/react-json-tree-0.6.11.tgz", "integrity": "sha512-HP0Sf0ZHjCi1FHLJxh/pLaxaevEW6ILlV2C5Dn3EZFTkLjWkv+EVf/l/zvtmoU9ZwuO/3TKVeWK/700UDxunTw==", + "dev": true, "requires": { "@types/react": "*" } diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 2ea7c6b13..861dacb9f 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -163,6 +163,7 @@ "@types/jest": "^23.3.5", "@types/react": "^16.4.18", "@types/react-dom": "^16.0.9", + "@types/react-json-tree": "^0.6.11", "@types/react-table": "^7.0.2", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index 8090cd099..51a0ca475 100644 --- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx +++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx @@ -38,6 +38,13 @@ describe('ListView', () => { filterable: true, }, ], + filters: [ + { + Header: 'Name', + id: 'name', + operators: [{ label: 'Starts With', value: 'sw' }], + }, + ], data: [ { id: 1, name: 'data 1' }, { id: 2, name: 'data 2' }, @@ -64,15 +71,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', () => { @@ -83,20 +90,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', () => { @@ -118,6 +125,7 @@ describe('ListView', () => { act(() => { wrapper.find('.filter-inputs input[type="text"]').prop('onChange')({ + persist() {}, currentTarget: { value: 'foo' }, }); }); @@ -132,27 +140,27 @@ describe('ListView', () => { wrapper.update(); expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Array [ - Object { - "Header": "name", - "filterId": "sw", - "id": "name", - "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', () => { @@ -162,27 +170,27 @@ describe('ListView', () => { wrapper.update(); expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Array [ - Object { - "Header": "name", - "filterId": "sw", - "id": "name", - "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(() => { @@ -207,15 +215,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(() => { @@ -240,18 +248,18 @@ 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", + }, + ], + ] + `); }); }); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index f7810d393..e624b56fe 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -17,7 +17,7 @@ * under the License. */ import { t } from '@superset-ui/translation'; -import React, { FunctionComponent, useMemo } from 'react'; +import React, { FunctionComponent } from 'react'; import { Button, Col, @@ -28,17 +28,26 @@ import { Row, // @ts-ignore } from 'react-bootstrap'; +// @ts-ignore +import SelectComponent from 'react-select'; +// @ts-ignore +import VirtualizedSelect from 'react-virtualized-select'; import IndeterminateCheckbox from '../IndeterminateCheckbox'; import './ListViewStyles.less'; import TableCollection from './TableCollection'; import { FetchDataConfig, - FilterToggle, - FilterType, - FilterTypeMap, + Filters, + InternalFilter, + Select, SortColumn, } from './types'; -import { convertFilters, removeFromList, useListViewState } from './utils'; +import { + convertFilters, + extractInputValue, + removeFromList, + useListViewState, +} from './utils'; interface Props { columns: any[]; @@ -50,7 +59,7 @@ interface Props { className?: string; title?: string; initialSort?: SortColumn[]; - filterTypes?: FilterTypeMap; + filters?: Filters; bulkActions?: Array<{ key?: string; name: React.ReactNode; @@ -82,7 +91,7 @@ const ListView: FunctionComponent = ({ initialSort = [], className = '', title = '', - filterTypes = {}, + filters = [], bulkActions = [], }) => { const { @@ -96,12 +105,12 @@ const ListView: FunctionComponent = ({ pageCount = 1, gotoPage, setAllFilters, - setFilterToggles, - updateFilterToggle, + setInternalFilters, + updateInternalFilter, applyFilters, filtersApplied, selectedFlatRows, - state: { pageIndex, pageSize, filterToggles }, + state: { pageIndex, pageSize, internalFilters }, } = useListViewState({ bulkSelectColumnConfig, bulkSelectMode: Boolean(bulkActions.length), @@ -112,14 +121,11 @@ const ListView: FunctionComponent = ({ initialPageSize, initialSort, }); - const filterableColumns = useMemo(() => columns.filter(c => c.filterable), [ - columns, - ]); - const filterable = Boolean(columns.length); + const filterable = Boolean(filters.length); const removeFilterAndApply = (index: number) => { - const updated = removeFromList(filterToggles, index); - setFilterToggles(updated); + const updated = removeFromList(internalFilters, index); + setInternalFilters(updated); setAllFilters(convertFilters(updated)); }; @@ -147,17 +153,17 @@ const ListView: FunctionComponent = ({ } > - {filterableColumns - .map(({ id, accessor, Header }) => ({ + {filters + .map(({ id, Header }) => ({ Header, - id: id || accessor, + id, })) - .map((ft: FilterToggle) => ( + .map((ft: InternalFilter) => ( - setFilterToggles([...filterToggles, fltr]) + onSelect={(fltr: InternalFilter) => + setInternalFilters([...internalFilters, fltr]) } > {ft.Header} @@ -169,62 +175,89 @@ const ListView: FunctionComponent = ({ )}
- {filterToggles.map((ft, i) => ( -
- - - {ft.Header} - - - ) => - updateFilterToggle(i, { filterId: e.currentTarget.value }) - } - > - {filterTypes[ft.id] && - filterTypes[ft.id].map( - ({ name, operator }: FilterType) => ( - - ), - )} - - - - - ) => - updateFilterToggle(i, { - value: e.currentTarget.value, - }) - } - /> - - -
removeFilterAndApply(i)} - > - -
- -
-
-
- ))} - {filterToggles.length > 0 && ( + {internalFilters.map((ft, i) => { + const filter = filters.find(f => f.id === ft.id); + if (!filter) { + console.error(`could not find filter for ${ft.id}`); + return null; + } + return ( +
+ + + {ft.Header} + + + ) => { + updateInternalFilter(i, { + operator: e.currentTarget.value, + }); + }} + > + {filter.operators.map(({ label, value }: Select) => ( + + ))} + + + + + {filter.input === 'select' && ( + { + updateInternalFilter(i, { + operator: ft.operator || filter.operators[0].value, + value: e ? e.map(s => s.value) : e, + }); + }} + /> + )} + {filter.input !== 'select' && ( + ) => { + e.persist(); + updateInternalFilter(i, { + operator: ft.operator || filter.operators[0].value, + value: extractInputValue(filter.input, e), + }); + }} + /> + )} + + +
removeFilterAndApply(i)} + > + +
+ +
+
+
+ ); + })} + {internalFilters.length > 0 && ( <> diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 244834081..294192db0 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -23,31 +23,50 @@ export interface SortColumn { export type SortColumns = SortColumn[]; +export interface Select { + label: string; + value: any; +} + export interface Filter { + Header: string; id: string; - filterId?: string; - value: string; + operators: Select[]; + input?: 'text' | 'textarea' | 'select' | 'checkbox'; + selects?: Select[]; } -export interface FilterType { - name: string; - operator: any; -} +export type Filters = Filter[]; -export interface FilterTypeMap { - [columnId: string]: FilterType[]; +export interface FilterValue { + id: string; + operator?: string; + value: string | boolean | number; } export interface FetchDataConfig { pageIndex: number; pageSize: number; sortBy: SortColumns; - filters: Filter[]; + filters: FilterValue[]; } -export interface FilterToggle { - id: string; +export interface InternalFilter extends FilterValue { Header: string; - filterId?: number; - value?: string; +} + +export interface FilterOperatorMap { + [columnId: string]: Array<{ + name: string; + operator: + | 'sw' + | 'ew' + | 'ct' + | 'eq' + | 'nsw' + | 'new' + | 'nct' + | 'neq' + | 'rel_m_m'; + }>; } diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 371bdf19b..ca4a88deb 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -33,7 +33,7 @@ import { useQueryParams, } from 'use-query-params'; -import { FetchDataConfig, FilterToggle, SortColumn } from './types'; +import { FetchDataConfig, InternalFilter, SortColumn } from './types'; // removes element from a list, returns new list export function removeFromList(list: any[], index: number): any[] { @@ -52,10 +52,24 @@ function updateInList(list: any[], index: number, update: any): any[] { } // convert filters from UI objects to data objects -export function convertFilters(fts: FilterToggle[]) { +export function convertFilters(fts: InternalFilter[]) { return fts - .filter((ft: FilterToggle) => ft.value) - .map(ft => ({ value: null, filterId: ft.filterId || 'sw', ...ft })); + .filter((ft: InternalFilter) => ft.value) + .map(ft => ({ operator: ft.operator, ...ft })); +} + +export function extractInputValue( + inputType: 'text' | 'textarea' | 'checkbox' | 'select' | undefined, + event: any, +) { + if (!inputType || inputType === 'text') { + return event.currentTarget.value; + } + if (inputType === 'checkbox') { + return event.currentTarget.checked; + } + + return null; } interface UseListViewConfig { @@ -128,6 +142,7 @@ export function useListViewState({ columns: columnsWithSelect, count, data, + disableFilters: true, disableSortRemove: true, initialState, manualFilters: true, @@ -142,13 +157,13 @@ export function useListViewState({ useRowSelect, ); - const [filterToggles, setFilterToggles] = useState( + const [internalFilters, setInternalFilters] = useState( query.filters || [], ); useEffect(() => { const queryParams: any = { - filters: filterToggles, + filters: internalFilters, pageIndex, }; if (sortBy[0]) { @@ -160,18 +175,18 @@ export function useListViewState({ fetchData({ pageIndex, pageSize, sortBy, filters }); }, [fetchData, pageIndex, pageSize, sortBy, filters]); - const filtersApplied = filterToggles.every( - ({ id, value, filterId }, index) => + const filtersApplied = internalFilters.every( + ({ id, value, operator }, index) => id && filters[index] && filters[index].id === id && filters[index].value === value && // @ts-ignore - filters[index].filterId === filterId, + filters[index].operator === operator, ); return { - applyFilters: () => setAllFilters(convertFilters(filterToggles)), + applyFilters: () => setAllFilters(convertFilters(internalFilters)), canNextPage, canPreviousPage, filtersApplied, @@ -184,9 +199,9 @@ export function useListViewState({ rows, selectedFlatRows, setAllFilters, - setFilterToggles, - state: { pageIndex, pageSize, sortBy, filters, filterToggles }, - updateFilterToggle: (index: number, update: object) => - setFilterToggles(updateInList(filterToggles, index, update)), + setInternalFilters, + state: { pageIndex, pageSize, sortBy, filters, internalFilters }, + updateInternalFilter: (index: number, update: object) => + setInternalFilters(updateInList(internalFilters, index, update)), }; } diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 34c260a19..aea0aace8 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -25,7 +25,11 @@ import React from 'react'; import { Panel } from 'react-bootstrap'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListView from 'src/components/ListView/ListView'; -import { FetchDataConfig, FilterTypeMap } from 'src/components/ListView/types'; +import { + FetchDataConfig, + FilterOperatorMap, + Filters, +} from 'src/components/ListView/types'; import withToasts from 'src/messageToasts/enhancers/withToasts'; const PAGE_SIZE = 25; @@ -36,12 +40,13 @@ interface Props { } interface State { - chartCount: number; charts: any[]; - filterTypes: FilterTypeMap; - labelColumns: { [key: string]: string }; - lastFetchDataConfig: FetchDataConfig | null; + chartCount: number; loading: boolean; + filterOperators: FilterOperatorMap; + filters: Filters; + owners: Array<{ text: string; value: number }>; + lastFetchDataConfig: FetchDataConfig | null; permissions: string[]; } @@ -62,22 +67,43 @@ class ChartList extends React.PureComponent { state: State = { chartCount: 0, charts: [], - filterTypes: {}, - labelColumns: {}, + filterOperators: {}, + filters: [], lastFetchDataConfig: null, loading: false, + owners: [], permissions: [], }; componentDidMount() { - SupersetClient.get({ - endpoint: `/api/v1/chart/_info`, - }).then(({ json = {} }) => { - this.setState({ - filterTypes: json.filters, - permissions: json.permissions, - }); - }); + Promise.all([ + SupersetClient.get({ + endpoint: `/api/v1/chart/_info`, + }), + SupersetClient.get({ + endpoint: `/api/v1/chart/related/owners`, + }), + ]).then( + ([{ json: infoJson = {} }, { json: ownersJson = {} }]) => { + this.setState( + { + filterOperators: infoJson.filters, + owners: ownersJson.result, + permissions: infoJson.permissions, + }, + this.updateFilters, + ); + }, + ([e1, e2]) => { + this.props.addDangerToast(t('An error occurred while fetching Charts')); + if (e1) { + console.error(e1); + } + if (e2) { + console.error(e2); + } + }, + ); } get canEdit() { @@ -99,7 +125,6 @@ class ChartList extends React.PureComponent { }: any) => {sliceName}, Header: t('Chart'), accessor: 'slice_name', - filterable: true, sortable: true, }, { @@ -274,11 +299,7 @@ class ChartList extends React.PureComponent { endpoint: `/api/v1/chart/?q=${queryParams}`, }) .then(({ json = {} }) => { - this.setState({ - charts: json.result, - chartCount: json.count, - labelColumns: json.label_columns, - }); + this.setState({ charts: json.result, chartCount: json.count }); }) .catch(() => { this.props.addDangerToast(t('An error occurred while fetching Charts')); @@ -288,8 +309,52 @@ class ChartList extends React.PureComponent { }); }; + updateFilters = () => { + const { filterOperators, owners } = this.state; + const convertFilter = ({ + name: label, + operator, + }: { + name: string; + operator: string; + }) => ({ label, value: operator }); + + this.setState({ + filters: [ + { + Header: 'Chart', + id: 'slice_name', + operators: filterOperators.slice_name.map(convertFilter), + }, + { + Header: 'Description', + id: 'description', + input: 'textarea', + operators: filterOperators.slice_name.map(convertFilter), + }, + { + Header: 'Visualization Type', + id: 'viz_type', + operators: filterOperators.viz_type.map(convertFilter), + }, + { + Header: 'Datasource Name', + id: 'datasource_name', + operators: filterOperators.datasource_name.map(convertFilter), + }, + { + Header: 'Owners', + id: 'owners', + input: 'select', + operators: filterOperators.owners.map(convertFilter), + selects: owners.map(({ text: label, value }) => ({ label, value })), + }, + ], + }); + }; + render() { - const { charts, chartCount, loading, filterTypes } = this.state; + const { charts, chartCount, loading, filters } = this.state; return (
@@ -324,7 +389,7 @@ class ChartList extends React.PureComponent { fetchData={this.fetchData} loading={loading} initialSort={this.initialSort} - filterTypes={filterTypes} + filters={filters} bulkActions={bulkActions} /> ); diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx b/superset-frontend/src/views/dashboardList/DashboardList.tsx index 929f0a31e..aec63c73d 100644 --- a/superset-frontend/src/views/dashboardList/DashboardList.tsx +++ b/superset-frontend/src/views/dashboardList/DashboardList.tsx @@ -25,7 +25,11 @@ import React from 'react'; import { Panel } from 'react-bootstrap'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListView from 'src/components/ListView/ListView'; -import { FetchDataConfig, FilterTypeMap } from 'src/components/ListView/types'; +import { + FetchDataConfig, + FilterOperatorMap, + Filters, +} from 'src/components/ListView/types'; import withToasts from 'src/messageToasts/enhancers/withToasts'; const PAGE_SIZE = 25; @@ -39,9 +43,10 @@ interface State { dashboards: any[]; dashboardCount: number; loading: boolean; - filterTypes: FilterTypeMap; + filterOperators: FilterOperatorMap; + filters: Filters; + owners: Array<{ text: string; value: number }>; permissions: string[]; - labelColumns: { [key: string]: string }; lastFetchDataConfig: FetchDataConfig | null; } @@ -64,22 +69,45 @@ class DashboardList extends React.PureComponent { state: State = { dashboardCount: 0, dashboards: [], - filterTypes: {}, - labelColumns: {}, + filterOperators: {}, + filters: [], lastFetchDataConfig: null, loading: false, + owners: [], permissions: [], }; componentDidMount() { - SupersetClient.get({ - endpoint: `/api/v1/dashboard/_info`, - }).then(({ json = {} }) => { - this.setState({ - filterTypes: json.filters, - permissions: json.permissions, - }); - }); + Promise.all([ + SupersetClient.get({ + endpoint: `/api/v1/dashboard/_info`, + }), + SupersetClient.get({ + endpoint: `/api/v1/dashboard/related/owners`, + }), + ]).then( + ([{ json: infoJson = {} }, { json: ownersJson = {} }]) => { + this.setState( + { + filterOperators: infoJson.filters, + owners: ownersJson.result, + permissions: infoJson.permissions, + }, + this.updateFilters, + ); + }, + ([e1, e2]) => { + this.props.addDangerToast( + t('An error occurred while fetching Dashboards'), + ); + if (e1) { + console.error(e1); + } + if (e2) { + console.error(e2); + } + }, + ); } get canEdit() { @@ -105,7 +133,6 @@ class DashboardList extends React.PureComponent { }: any) => {dashboardTitle}, Header: t('Title'), accessor: 'dashboard_title', - filterable: true, sortable: true, }, { @@ -117,7 +144,7 @@ class DashboardList extends React.PureComponent { }, }, }: any) => {changedByName}, - Header: t('Changed By Name'), + Header: t('Creator'), accessor: 'changed_by_fk', sortable: true, }, @@ -141,10 +168,16 @@ class DashboardList extends React.PureComponent { original: { changed_on: changedOn }, }, }: any) => {moment(changedOn).fromNow()}, - Header: t('Changed On'), + Header: t('Modified'), accessor: 'changed_on', sortable: true, }, + { + accessor: 'slug', + }, + { + accessor: 'owners', + }, { Cell: ({ row: { state, original } }: any) => { const handleDelete = () => this.handleDashboardDelete(original); @@ -265,9 +298,11 @@ class DashboardList extends React.PureComponent { }; handleBulkDashboardExport = (dashboards: Dashboard[]) => { - window.location.href = `/api/v1/dashboard/export/?q=!(${dashboards - .map(({ id }) => id) - .join(',')})`; + return window.location.assign( + `/api/v1/dashboard/export/?q=!(${dashboards + .map(({ id }) => id) + .join(',')})`, + ); }; fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => { @@ -281,9 +316,9 @@ class DashboardList extends React.PureComponent { }, loading: true, }); - const filterExps = filters.map(({ id, filterId, value }) => ({ - col: id, - opr: filterId, + const filterExps = filters.map(({ id: col, operator: opr, value }) => ({ + col, + opr, value, })); @@ -299,11 +334,7 @@ class DashboardList extends React.PureComponent { endpoint: `/api/v1/dashboard/?q=${queryParams}`, }) .then(({ json = {} }) => { - this.setState({ - dashboards: json.result, - dashboardCount: json.count, - labelColumns: json.label_columns, - }); + this.setState({ dashboards: json.result, dashboardCount: json.count }); }) .catch(() => { this.props.addDangerToast( @@ -315,8 +346,48 @@ class DashboardList extends React.PureComponent { }); }; + updateFilters = () => { + const { filterOperators, owners } = this.state; + const convertFilter = ({ + name: label, + operator, + }: { + name: string; + operator: string; + }) => ({ label, value: operator }); + + this.setState({ + filters: [ + { + Header: 'Dashboard', + id: 'dashboard_title', + operators: filterOperators.dashboard_title.map(convertFilter), + }, + { + Header: 'Slug', + id: 'slug', + operators: filterOperators.slug.map(convertFilter), + }, + { + Header: 'Owners', + id: 'owners', + input: 'select', + operators: filterOperators.owners.map(convertFilter), + selects: owners.map(({ text: label, value }) => ({ label, value })), + }, + { + Header: 'Published', + id: 'published', + input: 'checkbox', + operators: filterOperators.published.map(convertFilter), + }, + ], + }); + }; + render() { - const { dashboards, dashboardCount, loading, filterTypes } = this.state; + const { dashboards, dashboardCount, loading, filters } = this.state; + return (
@@ -362,7 +433,7 @@ class DashboardList extends React.PureComponent { fetchData={this.fetchData} loading={loading} initialSort={this.initialSort} - filterTypes={filterTypes} + filters={filters} bulkActions={bulkActions} /> ); diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py index 5e09019ec..87984b229 100644 --- a/superset/views/dashboard/api.py +++ b/superset/views/dashboard/api.py @@ -142,27 +142,28 @@ class DashboardRestApi(DashboardMixin, BaseOwnedModelRestApi): class_permission_name = "DashboardModelView" show_columns = [ + "charts", + "css", "dashboard_title", - "slug", + "json_metadata", "owners.id", "owners.username", "position_json", - "css", - "json_metadata", "published", + "slug", "table_names", - "charts", ] order_columns = ["dashboard_title", "changed_on", "published", "changed_by_fk"] list_columns = [ - "id", - "dashboard_title", - "url", - "published", - "changed_by.username", "changed_by_name", "changed_by_url", + "changed_by.username", "changed_on", + "dashboard_title", + "id", + "published", + "slug", + "url", ] add_model_schema = DashboardPostSchema()