diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json index 832355bca4..4755c513e0 100644 --- a/superset/assets/package-lock.json +++ b/superset/assets/package-lock.json @@ -4885,6 +4885,15 @@ "@types/webpack": "*" } }, + "@types/react-table": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.0.2.tgz", + "integrity": "sha512-sxvjV0JCk/ijCzENejXth99cFMnmucATaC31gz1bMk8iQwUDE2VYaw2QQTcDrzBxzastBQGdcLpcFIN61RvgIA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-virtualized": { "version": "9.21.5", "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.5.tgz", @@ -21048,9 +21057,9 @@ } }, "react-table": { - "version": "7.0.0-beta.26", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.0.0-beta.26.tgz", - "integrity": "sha512-Pw/1T9kiAjV1cIf6K6bQV6yNQc3O7XUGin1RcSR1xFKw0RNGC5vl1VDPZrNep1BXDsbR6o8O63X45HFNVg6HzA==" + "version": "7.0.0-rc.15", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.0.0-rc.15.tgz", + "integrity": "sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==" }, "react-test-renderer": { "version": "16.9.0", diff --git a/superset/assets/package.json b/superset/assets/package.json index 0312de2b48..c484547d8e 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -136,7 +136,7 @@ "react-split": "^2.0.4", "react-sticky": "^6.0.2", "react-syntax-highlighter": "^7.0.4", - "react-table": "^7.0.0-beta.26", + "react-table": "^7.0.0-rc.15", "react-transition-group": "^2.5.3", "react-virtualized": "9.19.1", "react-virtualized-select": "^3.1.3", @@ -162,6 +162,7 @@ "@types/jest": "^23.3.5", "@types/react": "^16.4.18", "@types/react-dom": "^16.0.9", + "@types/react-table": "^7.0.2", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.8.0", diff --git a/superset/assets/spec/javascripts/components/ConfirmStatusChange_spec.jsx b/superset/assets/spec/javascripts/components/ConfirmStatusChange_spec.jsx new file mode 100644 index 0000000000..ec3c7b8b39 --- /dev/null +++ b/superset/assets/spec/javascripts/components/ConfirmStatusChange_spec.jsx @@ -0,0 +1,60 @@ +/** + * 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 { Modal, Button } from 'react-bootstrap'; +import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; + +describe('ConfirmStatusChange', () => { + const mockedProps = { + title: 'please confirm', + description: 'are you sure?', + onConfirm: jest.fn(), + }; + const wrapper = mount( + + {confirm => ( + <> + + + + + + ); + } +} diff --git a/superset/assets/src/components/IndeterminateCheckbox.jsx b/superset/assets/src/components/IndeterminateCheckbox.jsx new file mode 100644 index 0000000000..31b1241824 --- /dev/null +++ b/superset/assets/src/components/IndeterminateCheckbox.jsx @@ -0,0 +1,38 @@ +/** + * 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'; + +const IndeterminateCheckbox = React.forwardRef( + ({ indeterminate, ...rest }, ref) => { + const defaultRef = React.useRef(); + const resolvedRef = ref || defaultRef; + + React.useEffect(() => { + resolvedRef.current.indeterminate = indeterminate; + }, [resolvedRef, indeterminate]); + + return ( + <> + + + ); + }, +); + +export default IndeterminateCheckbox; diff --git a/superset/assets/src/components/ListView/ListView.tsx b/superset/assets/src/components/ListView/ListView.tsx index f236d74c5c..5d3168ecf3 100644 --- a/superset/assets/src/components/ListView/ListView.tsx +++ b/superset/assets/src/components/ListView/ListView.tsx @@ -28,7 +28,7 @@ import { Row, // @ts-ignore } from 'react-bootstrap'; -import Loading from '../Loading'; +import IndeterminateCheckbox from '../IndeterminateCheckbox'; import './ListViewStyles.less'; import TableCollection from './TableCollection'; import { FetchDataConfig, FilterToggle, FilterType, FilterTypeMap, SortColumn } from './types'; @@ -45,8 +45,23 @@ interface Props { title?: string; initialSort?: SortColumn[]; filterTypes?: FilterTypeMap; + bulkActions?: Array<{ key?: string, name: React.ReactNode, onSelect: (rows: any[]) => any }>; } +const bulkSelectColumnConfig = { + Cell: ({ row }: any) => ( +
+ +
+ ), + Header: ({ getToggleAllRowsSelectedProps }: any) => ( +
+ +
+ ), + id: 'selection', +}; + const ListView: FunctionComponent = ({ columns, data, @@ -58,6 +73,7 @@ const ListView: FunctionComponent = ({ className = '', title = '', filterTypes = {}, + bulkActions = [], }) => { const { getTableProps, @@ -74,8 +90,11 @@ const ListView: FunctionComponent = ({ updateFilterToggle, applyFilters, filtersApplied, + selectedFlatRows, state: { pageIndex, pageSize, filterToggles }, } = useListViewState({ + bulkSelectColumnConfig, + bulkSelectMode: Boolean(bulkActions.length), columns, count, data, @@ -92,10 +111,6 @@ const ListView: FunctionComponent = ({ setAllFilters(convertFilters(updated)); }; - if (loading) { - return ; - } - return (
{title && filterable && ( @@ -108,6 +123,7 @@ const ListView: FunctionComponent = ({
= ({ {' '}{t('Filter List')} )} - id={'filter-picker'} > {filterableColumns .map(({ id, accessor, Header }) => ({ @@ -128,9 +143,8 @@ const ListView: FunctionComponent = ({ { - setFilterToggles([...filterToggles, fltr]); - } + onSelect={ + (fltr: FilterToggle) => setFilterToggles([...filterToggles, fltr]) } > {ft.Header} @@ -153,7 +167,7 @@ const ListView: FunctionComponent = ({ componentClass='select' bsSize='small' value={ft.filterId} - placeholder={filterTypes[ft.id][0].name} + placeholder={filterTypes[ft.id] ? filterTypes[ft.id][0].name : ''} onChange={(e: React.MouseEvent) => updateFilterToggle(i, { filterId: e.currentTarget.value }) } @@ -172,11 +186,12 @@ const ListView: FunctionComponent = ({ ) => - updateFilterToggle(i, { - filterValue: e.currentTarget.value, - }) + value={ft.value || ''} + onChange={ + (e: React.KeyboardEvent) => + updateFilterToggle(i, { + value: e.currentTarget.value, + }) } /> @@ -226,28 +241,67 @@ const ListView: FunctionComponent = ({ />
- 1} - next={canNextPage} - last={pageIndex < pageCount - 2} - items={pageCount} - activePage={pageIndex + 1} - ellipsis={true} - boundaryLinks={true} - maxButtons={5} - onSelect={(p: number) => gotoPage(p - 1)} - /> - - {t('showing')}{' '} - - {pageSize * pageIndex + (rows.length && 1)}- - {pageSize * pageIndex + rows.length} - {' '} - {t('of')} {count} - -
-
+ + +
+
+ {bulkActions.length > 0 && ( + + {t('Actions')} + + )} + > + {bulkActions.map((action) => ( + { + action.onSelect(selectedRows.map((r: any) => r.original)); + } + } + > + {action.name} + + ))} + + )} +
+
+ + + 1} + next={canNextPage} + last={pageIndex < pageCount - 2} + items={pageCount} + activePage={pageIndex + 1} + ellipsis={true} + boundaryLinks={true} + maxButtons={5} + onSelect={(p: number) => gotoPage(p - 1)} + /> + + + + {t('showing')}{' '} + + {pageSize * pageIndex + (rows.length && 1)}-{pageSize * pageIndex + rows.length} + {' '} + {t('of')} {count} + + +
+ + ); }; diff --git a/superset/assets/src/components/ListView/TableCollection.tsx b/superset/assets/src/components/ListView/TableCollection.tsx index f914b2bb92..1baae15cd0 100644 --- a/superset/assets/src/components/ListView/TableCollection.tsx +++ b/superset/assets/src/components/ListView/TableCollection.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { Cell, HeaderGroup, Row } from 'react-table'; -interface Props { +interface Props { getTableProps: (userProps?: any) => any; getTableBodyProps: (userProps?: any) => any; prepareRow: (row: Row) => any; diff --git a/superset/assets/src/components/ListView/types.ts b/superset/assets/src/components/ListView/types.ts index 0151d45159..2448340816 100644 --- a/superset/assets/src/components/ListView/types.ts +++ b/superset/assets/src/components/ListView/types.ts @@ -18,14 +18,15 @@ */ export interface SortColumn { id: string; - desc: boolean; + desc?: boolean; } export type SortColumns = SortColumn[]; export interface Filter { - filterId: number; - filterValue: string; + id: string; + filterId?: string; + value: string; } export interface FilterType { @@ -37,20 +38,16 @@ export interface FilterTypeMap { [columnId: string]: FilterType[]; } -interface FilterMap { - [columnId: string]: Filter; -} - export interface FetchDataConfig { pageIndex: number; pageSize: number; sortBy: SortColumns; - filters: FilterMap; + filters: Filter[]; } export interface FilterToggle { id: string; Header: string; filterId?: number; - filterValue?: string; + value?: string; } diff --git a/superset/assets/src/components/ListView/utils.ts b/superset/assets/src/components/ListView/utils.ts index d62de94075..cb0fdc838d 100644 --- a/superset/assets/src/components/ListView/utils.ts +++ b/superset/assets/src/components/ListView/utils.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useFilters, usePagination, + useRowSelect, useRowState, useSortBy, useTable, @@ -53,14 +54,8 @@ function updateInList(list: any[], index: number, update: any): any[] { // convert filters from UI objects to data objects export function convertFilters(fts: FilterToggle[]) { return fts - .filter((ft: FilterToggle) => ft.filterValue) - .reduce((acc, ft) => { - acc[ft.id] = { - filterId: ft.filterId || 'sw', - filterValue: ft.filterValue, - }; - return acc; - }, {}); + .filter((ft: FilterToggle) => ft.value) + .map((ft) => ({ value: null, filterId: ft.filterId || 'sw', ...ft })); } interface UseListViewConfig { @@ -70,6 +65,12 @@ interface UseListViewConfig { count: number; initialPageSize: number; initialSort?: SortColumn[]; + bulkSelectMode?: boolean; + bulkSelectColumnConfig?: { + id: string; + Header: (conf: any) => React.ReactNode; + Cell: (conf: any) => React.ReactNode; + }; } export function useListViewState({ @@ -79,6 +80,8 @@ export function useListViewState({ count, initialPageSize, initialSort = [], + bulkSelectMode = false, + bulkSelectColumnConfig, }: UseListViewConfig) { const [query, setQuery] = useQueryParams({ filters: JsonParam, @@ -87,6 +90,26 @@ export function useListViewState({ sortOrder: StringParam, }); + const initialSortBy = useMemo( + () => + query.sortColumn && query.sortOrder + ? [{ id: query.sortColumn, desc: query.sortOrder === 'desc' }] + : initialSort, + [query.sortColumn, query.sortOrder], + ); + + const initialState = { + filters: convertFilters(query.filters || []), + pageIndex: query.pageIndex || 0, + pageSize: initialPageSize, + sortBy: initialSortBy, + }; + + const columnsWithSelect = useMemo( + () => (bulkSelectMode ? [bulkSelectColumnConfig, ...columns] : columns), + [bulkSelectMode, columns], + ); + const { getTableProps, getTableBodyProps, @@ -98,31 +121,25 @@ export function useListViewState({ pageCount, gotoPage, setAllFilters, + selectedFlatRows, state: { pageIndex, pageSize, sortBy, filters }, } = useTable( { - columns, + columns: columnsWithSelect, count, data, disableSortRemove: true, - initialState: { - filters: convertFilters(query.filters || []), - pageIndex: query.pageIndex || 0, - pageSize: initialPageSize, - sortBy: - query.sortColumn && query.sortOrder - ? [{ id: query.sortColumn, desc: query.sortOrder === 'desc' }] - : initialSort, - }, + initialState, manualFilters: true, manualPagination: true, - manualSorting: true, + manualSortBy: true, pageCount: Math.ceil(count / initialPageSize), }, useFilters, useSortBy, usePagination, useRowState, + useRowSelect, ); const [filterToggles, setFilterToggles] = useState( @@ -144,11 +161,13 @@ export function useListViewState({ }, [fetchData, pageIndex, pageSize, sortBy, filters]); const filtersApplied = filterToggles.every( - ({ id, filterValue, filterId = 'sw' }) => + ({ id, value, filterId }, index) => id && - filters[id] && - filters[id].filterValue === filterValue && - filters[id].filterId === filterId, + filters[index] && + filters[index].id === id && + filters[index].value === value && + // @ts-ignore + filters[index].filterId === filterId, ); return { @@ -163,6 +182,7 @@ export function useListViewState({ pageCount, prepareRow, rows, + selectedFlatRows, setAllFilters, setFilterToggles, state: { pageIndex, pageSize, sortBy, filters, filterToggles }, diff --git a/superset/assets/src/types/react-table-config.d.ts b/superset/assets/src/types/react-table-config.d.ts new file mode 100644 index 0000000000..262f4e3716 --- /dev/null +++ b/superset/assets/src/types/react-table-config.d.ts @@ -0,0 +1,137 @@ +/** + * 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 { + UseColumnOrderInstanceProps, + UseColumnOrderState, + UseExpandedHooks, + UseExpandedInstanceProps, + UseExpandedOptions, + UseExpandedRowProps, + UseExpandedState, + UseFiltersColumnOptions, + UseFiltersColumnProps, + UseFiltersInstanceProps, + UseFiltersOptions, + UseFiltersState, + UseGlobalFiltersInstanceProps, + UseGlobalFiltersOptions, + UseGlobalFiltersState, + UseGroupByCellProps, + UseGroupByColumnOptions, + UseGroupByColumnProps, + UseGroupByHooks, + UseGroupByInstanceProps, + UseGroupByOptions, + UseGroupByRowProps, + UseGroupByState, + UsePaginationInstanceProps, + UsePaginationOptions, + UsePaginationState, + UseResizeColumnsColumnOptions, + UseResizeColumnsColumnProps, + UseResizeColumnsOptions, + UseResizeColumnsState, + UseRowSelectHooks, + UseRowSelectInstanceProps, + UseRowSelectOptions, + UseRowSelectRowProps, + UseRowSelectState, + UseRowStateCellProps, + UseRowStateInstanceProps, + UseRowStateOptions, + UseRowStateRowProps, + UseRowStateState, + UseSortByColumnOptions, + UseSortByColumnProps, + UseSortByHooks, + UseSortByInstanceProps, + UseSortByOptions, + UseSortByState, +} from 'react-table'; + +declare module 'react-table' { + export interface TableOptions + extends UseExpandedOptions, + UseFiltersOptions, + UseFiltersOptions, + UseGlobalFiltersOptions, + UseGroupByOptions, + UsePaginationOptions, + UseResizeColumnsOptions, + UseRowSelectOptions, + UseRowStateOptions, + UseSortByOptions, + // note that having Record here allows you to add anything to the options, this matches the spirit of the + // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your + // feature set, this is a safe default. + Record {} + + export interface Hooks + extends UseExpandedHooks, + UseGroupByHooks, + UseRowSelectHooks, + UseSortByHooks {} + + export interface TableInstance + extends UseColumnOrderInstanceProps, + UseExpandedInstanceProps, + UseFiltersInstanceProps, + UseGlobalFiltersInstanceProps, + UseGroupByInstanceProps, + UsePaginationInstanceProps, + UseRowSelectInstanceProps, + UseRowStateInstanceProps, + UseSortByInstanceProps {} + + export interface TableState + extends UseColumnOrderState, + UseExpandedState, + UseFiltersState, + UseGlobalFiltersState, + UseGroupByState, + UsePaginationState, + UseResizeColumnsState, + UseRowSelectState, + UseRowStateState, + UseSortByState {} + + export interface Column + extends UseFiltersColumnOptions, + UseGroupByColumnOptions, + UseResizeColumnsColumnOptions, + UseSortByColumnOptions { + cellProps?: any; + } + + export interface ColumnInstance + extends UseFiltersColumnProps, + UseGroupByColumnProps, + UseResizeColumnsColumnProps, + UseSortByColumnProps {} + + export interface Cell + extends UseGroupByCellProps, + UseRowStateCellProps {} + + export interface Row + extends UseExpandedRowProps, + UseGroupByRowProps, + UseRowSelectRowProps, + UseRowStateRowProps {} +} diff --git a/superset/assets/src/types/react-table.d.ts b/superset/assets/src/types/react-table.d.ts deleted file mode 100644 index 5a1e727584..0000000000 --- a/superset/assets/src/types/react-table.d.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// Type definitions for react-table 7 -// Project: https://github.com/tannerlinsley/react-table#readme -// Definitions by: Adrien Denat -// Artem Berdyshev -// Christian Murphy -// Tai Dupreee -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 3.0 -declare module 'react-table' { - import { Dispatch, ReactNode, SetStateAction } from 'react'; - - export interface Cell { - render: (type: string) => any; - getCellProps: () => { key: string; [k: string]: any }; - column: Column; - row: Row; - state: any; - value: any; - } - - export interface Row { - index: number; - cells: Array>; - getRowProps: () => { key: string; [k: string]: any }; - original: any; - state?: any; - setState?: (state: any) => any; - } - - export interface HeaderColumn { - /** - * This string/function is used to build the data model for your column. - */ - accessor: A | ((originalRow: D) => string); - Header?: string | ((props: TableInstance) => ReactNode); - Filter?: string | ((props: TableInstance) => ReactNode); - Cell?: string | ((cell: Cell) => ReactNode); - - /** - * This is the unique ID for the column. It is used by reference in things like sorting, grouping, filtering etc. - */ - id?: string | number; - minWidth?: string | number; - maxWidth?: string | number; - width?: string | number; - canSortBy?: boolean; - sortByFn?: (a: any, b: any, desc: boolean) => 0 | 1 | -1; - defaultSortDesc?: boolean; - [key: string]: any; - } - - export interface Column - extends HeaderColumn { - id: string | number; - } - - export type Page = Array>; - - export interface EnhancedColumn - extends Column { - render: (type: string) => any; - getHeaderProps: (userProps?: any) => any; - getSortByToggleProps: (userProps?: any) => any; - sorted: boolean; - sortedDesc: boolean; - sortedIndex: number; - } - - export interface HeaderGroup { - headers: Array>; - getRowProps: (userProps?: any) => any; - getHeaderGroupProps: (userProps?: any) => any; - } - - export interface Hooks { - beforeRender: []; - columns: []; - headerGroups: []; - headers: []; - rows: Array>; - row: []; - renderableRows: []; - getTableProps: []; - getRowProps: []; - getHeaderRowProps: []; - getHeaderProps: []; - getCellProps: []; - } - - export interface TableInstance - extends TableOptions, - UseRowsValues, - UseFiltersValues, - UsePaginationValues, - UseColumnsValues, - UseRowStateValues { - hooks: Hooks; - rows: Array>; - columns: Array>; - getTableProps: (userProps?: any) => any; - getTableBodyProps: (userProps?: any) => any; - getRowProps: (userProps?: any) => any; - prepareRow: (row: Row) => any; - getSelectRowToggleProps: (userProps?: any) => any; - toggleSelectAll: (forcedState: boolean) => any; - state: { [key: string]: any }; - } - - export interface TableOptions { - data: D[]; - columns: Array>; - state?: { [key: string]: any }; - debug?: boolean; - sortByFn?: (a: any, b: any, desc: boolean) => 0 | 1 | -1; - manualSorting?: boolean; - manualFilters?: boolean; - manualPagination?: boolean; - pageCount?: number; - disableSorting?: boolean; - defaultSortDesc?: boolean; - disableMultiSort?: boolean; - count?: number; - disableSortRemove?: boolean; - initialState?: any; - } - - export interface RowsProps { - subRowsKey: string; - } - - export interface FiltersProps { - filterFn: () => void; - manualFilters: boolean; - disableFilters: boolean; - setFilter: (columnId: string, filter: string) => any; - setAllFilters: (filterObj: any) => any; - } - - export interface UsePaginationValues { - nextPage: () => any; - previousPage: () => any; - setPageSize: (size: number) => any; - gotoPage: (page: number) => any; - canPreviousPage: boolean; - canNextPage: boolean; - page: Page; - pageOptions: []; - } - - export interface UseRowsValues { - rows: Array>; - } - - export interface UseColumnsValues { - columns: Array>; - headerGroups: Array>; - headers: Array>; - } - - export interface UseFiltersValues { - setFilter: (columnId: string, filter: string) => any; - setAllFilters: (filterObj: any) => any; - } - - export interface UseRowStateValues { - setRowState: (rowPath: string[], updater: (state: any) => any) => any; - } - - export function useTable( - props: TableOptions, - ...plugins: any[] - ): TableInstance; - - export function useColumns( - props: TableOptions, - ): TableOptions & UseColumnsValues; - - export function useRows( - props: TableOptions, - ): TableOptions & UseRowsValues; - - export function useFilters( - props: TableOptions, - ): TableOptions & { - rows: Array>; - }; - - export function useSortBy( - props: TableOptions, - ): TableOptions & { - rows: Array>; - }; - - export function useGroupBy( - props: TableOptions, - ): TableOptions & { rows: Array> }; - - export function usePagination( - props: TableOptions, - ): UsePaginationValues; - - export function useRowState(props: TableOptions): UseRowStateValues; - - export function useFlexLayout(props: TableOptions): TableOptions; - - export function useExpanded( - props: TableOptions, - ): TableOptions & { - toggleExpandedByPath: () => any; - expandedDepth: []; - rows: []; - }; - - export function useTableState( - initialState?: any, - overriddenState?: any, - options?: { - reducer?: (oldState: any, newState: any, type: string) => any; - useState?: [any, Dispatch>]; - }, - ): any; - - export const actions: any; -} diff --git a/superset/assets/src/views/dashboardList/DashboardList.tsx b/superset/assets/src/views/dashboardList/DashboardList.tsx index 0bc2823aee..2d9c6df367 100644 --- a/superset/assets/src/views/dashboardList/DashboardList.tsx +++ b/superset/assets/src/views/dashboardList/DashboardList.tsx @@ -23,9 +23,9 @@ import PropTypes from 'prop-types'; import React from 'react'; // @ts-ignore import { Button, Modal, Panel } from 'react-bootstrap'; +import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListView from 'src/components/ListView/ListView'; -import { FilterTypeMap } from 'src/components/ListView/types'; -import { FetchDataConfig } from 'src/components/ListView/types'; +import { FetchDataConfig, FilterTypeMap } from 'src/components/ListView/types'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import './DashboardList.less'; @@ -34,18 +34,30 @@ const PAGE_SIZE = 25; interface Props { addDangerToast: (msg: string) => void; + addSuccessToast: (msg: string) => void; } interface State { dashboards: any[]; dashboardCount: number; loading: boolean; - showDeleteModal: boolean; - deleteCandidate: any; filterTypes: FilterTypeMap; permissions: string[]; labelColumns: { [key: string]: string }; + lastFetchDataConfig: FetchDataConfig | null; } + +interface Dashboard { + id: number; + changed_by: string; + changed_by_name: string; + changed_by_url: string; + changed_on: string; + dashboard_title: string; + published: boolean; + url: string; +} + class DashboardList extends React.PureComponent { get canEdit() { @@ -56,6 +68,10 @@ class DashboardList extends React.PureComponent { return this.hasPerm('can_delete'); } + get canExport() { + return this.hasPerm('can_mulexport'); + } + public static propTypes = { addDangerToast: PropTypes.func.isRequired, }; @@ -63,148 +79,156 @@ class DashboardList extends React.PureComponent { public state: State = { dashboardCount: 0, dashboards: [], - deleteCandidate: {}, filterTypes: {}, labelColumns: {}, + lastFetchDataConfig: null, loading: false, permissions: [], - showDeleteModal: false, }; - public columns: any = []; - public initialSort = [{ id: 'changed_on', desc: true }]; - constructor(props: Props) { - super(props); - this.setColumns(); - } - - public setColumns = () => { - this.columns = [ - { - Cell: ({ - row: { - original: { url, dashboard_title }, - }, - }: any) => {dashboard_title}, - Header: this.state.labelColumns.dashboard_title || '', - accessor: 'dashboard_title', - filterable: true, - sortable: true, - }, - { - Cell: ({ - row: { - original: { changed_by_name, changed_by_url }, - }, - }: any) => {changed_by_name}, - Header: this.state.labelColumns.changed_by_name || '', - accessor: 'changed_by_fk', - sortable: true, - }, - { - Cell: ({ - row: { - original: { published }, - }, - }: any) => ( - {published ? : ''} - ), - Header: this.state.labelColumns.published || '', - accessor: 'published', - sortable: true, - }, - { - Cell: ({ - row: { - original: { changed_on }, - }, - }: any) => ( - {moment(changed_on).fromNow()} - ), - Header: this.state.labelColumns.changed_on || '', - accessor: 'changed_on', - sortable: true, - }, - { - Cell: ({ row: { state, original } }: any) => { - const handleDelete = () => this.handleDashboardDeleteConfirm(original); - const handleEdit = () => this.handleDashboardEdit(original); - if (!this.canEdit && !this.canDelete) { - return null; - } - - return ( - - {this.canDelete && ( - - - - )} - {this.canEdit && ( - - - - )} - - ); + public columns = [ + { + Cell: ({ + row: { + original: { url, dashboard_title }, }, - Header: 'Actions', - id: 'actions', + }: any) => {dashboard_title}, + Header: t('Title'), + accessor: 'dashboard_title', + filterable: true, + sortable: true, + }, + { + Cell: ({ + row: { + original: { changed_by_name, changed_by_url }, + }, + }: any) => {changed_by_name}, + Header: t('Changed By Name'), + accessor: 'changed_by_fk', + sortable: true, + }, + { + Cell: ({ + row: { + original: { published }, + }, + }: any) => ( + {published ? : ''} + ), + Header: t('Published'), + accessor: 'published', + sortable: true, + }, + { + Cell: ({ + row: { + original: { changed_on }, + }, + }: any) => ( + {moment(changed_on).fromNow()} + ), + Header: t('Changed On'), + accessor: 'changed_on', + sortable: 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) { + return null; + } + return ( + + {this.canDelete && ( + {t('Are you sure you want to delete')} {original.dashboard_title}?} + onConfirm={handleDelete} + > + {(confirmDelete) => ( + + + + )} + + )} + {this.canExport && ( + + + + )} + {this.canEdit && ( + + + + )} + + ); }, - ]; - } - - public hasPerm = (perm: string) => { - if (!this.state.permissions.length) { - return false; - } - - return Boolean(this.state.permissions.find((p) => p === perm)); - } + Header: t('Actions'), + id: 'actions', + }, + ]; public handleDashboardEdit = ({ id }: { id: number }) => { window.location.assign(`/dashboard/edit/${id}`); } - public handleDashboardDeleteConfirm = (dashboard: any) => { - this.setState({ - deleteCandidate: dashboard, - showDeleteModal: true, - }); - } - - public handleDashboardDelete = () => { - const { id, title } = this.state.deleteCandidate; - SupersetClient.delete({ + public handleDashboardDelete = ({ id, dashboard_title }: Dashboard) => { + return SupersetClient.delete({ endpoint: `/api/v1/dashboard/${id}`, }).then( - (resp) => { - const dashboards = this.state.dashboards.filter((d) => d.id !== id); - this.setState({ - dashboards, - deleteCandidate: {}, - showDeleteModal: false, - }); + () => { + const { lastFetchDataConfig } = this.state; + if (lastFetchDataConfig) { + this.fetchData(lastFetchDataConfig); + } + this.props.addSuccessToast(t('Deleted') + ` ${dashboard_title}`); }, (err: any) => { - this.props.addDangerToast(t('There was an issue deleting') + `${title}`); - this.setState({ showDeleteModal: false, deleteCandidate: {} }); + console.error(err); + this.props.addDangerToast(t('There was an issue deleting') + `${dashboard_title}`); }, ); } - public toggleModal = () => { - this.setState({ showDeleteModal: !this.state.showDeleteModal }); + public handleBulkDashboardDelete = (dashboards: Dashboard[]) => { + SupersetClient.delete({ + endpoint: `/api/v1/dashboard/?q=!(${dashboards.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 dashboards')); + }, + ); + } + + public handleBulkDashboardExport = (dashboards: Dashboard[]) => { + return window.location.href = `/ api / v1 / dashboard /export/?q=!(${dashboards.map(({ id }) => id).join(',')})`; } public fetchData = ({ @@ -213,11 +237,20 @@ class DashboardList extends React.PureComponent { sortBy, filters, }: FetchDataConfig) => { - this.setState({ loading: true }); - const filterExps = Object.keys(filters).map((fk) => ({ - col: fk, - opr: filters[fk].filterId, - value: filters[fk].filterValue, + // 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, filterId, value }) => ({ + col: id, + opr: filterId, + value, })); const queryParams = JSON.stringify({ @@ -240,7 +273,6 @@ class DashboardList extends React.PureComponent { ); }) .finally(() => { - this.setColumns(); this.setState({ loading: false }); }); } @@ -257,38 +289,58 @@ class DashboardList extends React.PureComponent { public render() { const { dashboards, dashboardCount, loading, filterTypes } = this.state; return ( -
+
- + + {(confirmDelete) => { + const bulkActions = []; + if (this.canDelete) { + bulkActions.push({ + key: 'delete', + name: <> Delete, + onSelect: confirmDelete, + }); + } + if (this.canExport) { + bulkActions.push({ + key: 'export', + name: <> Export, + onSelect: this.handleBulkDashboardExport, + }); + } + return ( + + ); + }} + - - - - - {t('Are you sure you want to delete')}{' '} - {this.state.deleteCandidate.dashboard_title}? - - - - - -
); } + + private hasPerm = (perm: string) => { + if (!this.state.permissions.length) { + return false; + } + + return Boolean(this.state.permissions.find((p) => p === perm)); + } } export default withToasts(DashboardList); diff --git a/superset/assets/tslint.json b/superset/assets/tslint.json index 021699e819..bd9c88f896 100644 --- a/superset/assets/tslint.json +++ b/superset/assets/tslint.json @@ -1,11 +1,14 @@ { "extends": ["tslint:recommended", "tslint-react"], - "jsRules": {}, + "jsRules": { + "no-console": false + }, "rules": { "interface-name": [true, "never-prefix"], "quotemark": [true, "single"], "jsx-no-multiline-js": false, - "jsx-no-lambda": false + "jsx-no-lambda": false, + "no-console": false }, "rulesDirectory": [] }