mirror of https://github.com/apache/superset.git
[dashboard] new, bulk actions for delete & export (#8979)
* bulk actions for dashboards list view * add confirm component * finish bulk actions work * remove loading component * fix sortby double render bug, lint, fix specs * adds spec for bulk actions * fix spec * spec ConfirmStatusChange * lint * tslint * address review feedback * tslint fixes * guard against empty filterTypes * persist dom events * tslint
This commit is contained in:
parent
aecc82e174
commit
a0cda321b7
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
<ConfirmStatusChange {...mockedProps}>
|
||||
{confirm => (
|
||||
<>
|
||||
<button id="btn1" onClick={confirm} />
|
||||
</>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
);
|
||||
|
||||
it('opens a confirm modal', () => {
|
||||
wrapper
|
||||
.find('#btn1')
|
||||
.props()
|
||||
.onClick('foo');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(Modal).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls the function on confirm', () => {
|
||||
wrapper
|
||||
.find(Button)
|
||||
.last()
|
||||
.props()
|
||||
.onClick();
|
||||
|
||||
expect(mockedProps.onConfirm).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
});
|
|
@ -50,47 +50,53 @@ describe('ListView', () => {
|
|||
id: [],
|
||||
name: [{ name: 'sw', label: 'Starts With' }],
|
||||
},
|
||||
bulkActions: [{ name: 'do something', onSelect: jest.fn() }],
|
||||
};
|
||||
const wrapper = mount(<ListView {...mockedProps} />);
|
||||
|
||||
afterEach(() => {
|
||||
mockedProps.fetchData.mockClear();
|
||||
mockedProps.bulkActions.forEach(ba => {
|
||||
ba.onSelect.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetchData on mount', () => {
|
||||
expect(wrapper.find(ListView)).toHaveLength(1);
|
||||
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Object {},
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('calls fetchData on sort', () => {
|
||||
wrapper
|
||||
.find('[data-test="sort-header"]')
|
||||
.first()
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
|
||||
expect(mockedProps.fetchData).toHaveBeenCalled();
|
||||
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Object {},
|
||||
"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', () => {
|
||||
|
@ -98,11 +104,13 @@ describe('ListView', () => {
|
|||
wrapper
|
||||
.find('.dropdown-toggle')
|
||||
.children('button')
|
||||
.at(0)
|
||||
.props()
|
||||
.onClick();
|
||||
|
||||
wrapper
|
||||
.find(MenuItem)
|
||||
.at(0)
|
||||
.props()
|
||||
.onSelect({ id: 'name', Header: 'name' });
|
||||
});
|
||||
|
@ -124,25 +132,27 @@ describe('ListView', () => {
|
|||
wrapper.update();
|
||||
|
||||
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Object {
|
||||
"name": Object {
|
||||
"filterId": "sw",
|
||||
"filterValue": "foo",
|
||||
},
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"Header": "name",
|
||||
"filterId": "sw",
|
||||
"id": "name",
|
||||
"value": "foo",
|
||||
},
|
||||
],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('calls fetchData on page change', () => {
|
||||
|
@ -152,23 +162,95 @@ 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('handles bulk actions on 1 row', () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('input[title="Toggle Row Selected"]')
|
||||
.at(0)
|
||||
.prop('onChange')({ target: { value: 'on' } });
|
||||
|
||||
wrapper
|
||||
.find('.dropdown-toggle')
|
||||
.children('button')
|
||||
.at(1)
|
||||
.props()
|
||||
.onClick();
|
||||
});
|
||||
wrapper.update();
|
||||
const bulkActionsProps = wrapper
|
||||
.find(MenuItem)
|
||||
.last()
|
||||
.props();
|
||||
|
||||
bulkActionsProps.onSelect(bulkActionsProps.eventKey);
|
||||
expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
"name": "data 1",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('handles bulk actions on all rows', () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('input[title="Toggle All Rows Selected"]')
|
||||
.at(0)
|
||||
.prop('onChange')({ target: { value: 'on' } });
|
||||
|
||||
wrapper
|
||||
.find('.dropdown-toggle')
|
||||
.children('button')
|
||||
.at(1)
|
||||
.props()
|
||||
.onClick();
|
||||
});
|
||||
wrapper.update();
|
||||
const bulkActionsProps = wrapper
|
||||
.find(MenuItem)
|
||||
.last()
|
||||
.props();
|
||||
|
||||
bulkActionsProps.onSelect(bulkActionsProps.eventKey);
|
||||
expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Object {
|
||||
"name": Object {
|
||||
"filterId": "sw",
|
||||
"filterValue": "foo",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
"name": "data 1",
|
||||
},
|
||||
"pageIndex": 1,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"id": 2,
|
||||
"name": "data 2",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -23,8 +23,7 @@ import configureStore from 'redux-mock-store';
|
|||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import DashboardTable from '../../../src/welcome/DashboardTable';
|
||||
import Loading from '../../../src/components/Loading';
|
||||
import DashboardTable from 'src/welcome/DashboardTable';
|
||||
|
||||
// store needed for withToasts(DashboardTable)
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
@ -43,11 +42,6 @@ function setup() {
|
|||
describe('DashboardTable', () => {
|
||||
beforeEach(fetchMock.resetHistory);
|
||||
|
||||
it('renders a Loading initially', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('fetches dashboards and renders a ListView', done => {
|
||||
const wrapper = setup();
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* 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 { t } from '@superset-ui/translation';
|
||||
import * as React from 'react';
|
||||
// @ts-ignore
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
|
||||
type Callback = (...args: any[]) => void;
|
||||
interface Props {
|
||||
title: string | React.ReactNode;
|
||||
description: string | React.ReactNode;
|
||||
onConfirm: Callback;
|
||||
children: (showConfirm: Callback) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
callbackArgs: any[];
|
||||
open: boolean;
|
||||
}
|
||||
export default class ConfirmStatusChange extends React.Component<Props, State> {
|
||||
|
||||
public state = {
|
||||
callbackArgs: [],
|
||||
open: false,
|
||||
};
|
||||
|
||||
public showConfirm = (...callbackArgs: any[]) => {
|
||||
// check if any args are DOM events, if so, call persist
|
||||
callbackArgs.forEach((arg) => arg && typeof arg.persist === 'function' && arg.persist());
|
||||
|
||||
this.setState({
|
||||
callbackArgs,
|
||||
open: true,
|
||||
});
|
||||
}
|
||||
|
||||
public hide = () => this.setState({ open: false, callbackArgs: [] });
|
||||
|
||||
public confirm = () => {
|
||||
this.props.onConfirm(...this.state.callbackArgs);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.props.children && this.props.children(this.showConfirm)}
|
||||
<Modal show={this.state.open} onHide={this.hide}>
|
||||
<Modal.Header closeButton={true} >{this.props.title}</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.props.description}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.hide}>{t('Cancel')}</Button>
|
||||
<Button bsStyle='danger' onClick={this.confirm}>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<>
|
||||
<input type="checkbox" ref={resolvedRef} {...rest} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default IndeterminateCheckbox;
|
|
@ -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) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
id: 'selection',
|
||||
};
|
||||
|
||||
const ListView: FunctionComponent<Props> = ({
|
||||
columns,
|
||||
data,
|
||||
|
@ -58,6 +73,7 @@ const ListView: FunctionComponent<Props> = ({
|
|||
className = '',
|
||||
title = '',
|
||||
filterTypes = {},
|
||||
bulkActions = [],
|
||||
}) => {
|
||||
const {
|
||||
getTableProps,
|
||||
|
@ -74,8 +90,11 @@ const ListView: FunctionComponent<Props> = ({
|
|||
updateFilterToggle,
|
||||
applyFilters,
|
||||
filtersApplied,
|
||||
selectedFlatRows,
|
||||
state: { pageIndex, pageSize, filterToggles },
|
||||
} = useListViewState({
|
||||
bulkSelectColumnConfig,
|
||||
bulkSelectMode: Boolean(bulkActions.length),
|
||||
columns,
|
||||
count,
|
||||
data,
|
||||
|
@ -92,10 +111,6 @@ const ListView: FunctionComponent<Props> = ({
|
|||
setAllFilters(convertFilters(updated));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`superset-list-view ${className}`}>
|
||||
{title && filterable && (
|
||||
|
@ -108,6 +123,7 @@ const ListView: FunctionComponent<Props> = ({
|
|||
<Col md={2}>
|
||||
<div className='filter-dropdown'>
|
||||
<DropdownButton
|
||||
id='filter-picker'
|
||||
bsSize='small'
|
||||
bsStyle={'default'}
|
||||
noCaret={true}
|
||||
|
@ -117,7 +133,6 @@ const ListView: FunctionComponent<Props> = ({
|
|||
{' '}{t('Filter List')}
|
||||
</>
|
||||
)}
|
||||
id={'filter-picker'}
|
||||
>
|
||||
{filterableColumns
|
||||
.map(({ id, accessor, Header }) => ({
|
||||
|
@ -128,9 +143,8 @@ const ListView: FunctionComponent<Props> = ({
|
|||
<MenuItem
|
||||
key={ft.id}
|
||||
eventKey={ft}
|
||||
onSelect={(fltr: FilterToggle) => {
|
||||
setFilterToggles([...filterToggles, fltr]);
|
||||
}
|
||||
onSelect={
|
||||
(fltr: FilterToggle) => setFilterToggles([...filterToggles, fltr])
|
||||
}
|
||||
>
|
||||
{ft.Header}
|
||||
|
@ -153,7 +167,7 @@ const ListView: FunctionComponent<Props> = ({
|
|||
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<HTMLInputElement>) =>
|
||||
updateFilterToggle(i, { filterId: e.currentTarget.value })
|
||||
}
|
||||
|
@ -172,11 +186,12 @@ const ListView: FunctionComponent<Props> = ({
|
|||
<FormControl
|
||||
type='text'
|
||||
bsSize='small'
|
||||
value={ft.filterValue || ''}
|
||||
onChange={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
updateFilterToggle(i, {
|
||||
filterValue: e.currentTarget.value,
|
||||
})
|
||||
value={ft.value || ''}
|
||||
onChange={
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
updateFilterToggle(i, {
|
||||
value: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -226,28 +241,67 @@ const ListView: FunctionComponent<Props> = ({
|
|||
/>
|
||||
</div>
|
||||
<div className='footer'>
|
||||
<Pagination
|
||||
prev={canPreviousPage}
|
||||
first={pageIndex > 1}
|
||||
next={canNextPage}
|
||||
last={pageIndex < pageCount - 2}
|
||||
items={pageCount}
|
||||
activePage={pageIndex + 1}
|
||||
ellipsis={true}
|
||||
boundaryLinks={true}
|
||||
maxButtons={5}
|
||||
onSelect={(p: number) => gotoPage(p - 1)}
|
||||
/>
|
||||
<span className='pull-right'>
|
||||
{t('showing')}{' '}
|
||||
<strong>
|
||||
{pageSize * pageIndex + (rows.length && 1)}-
|
||||
{pageSize * pageIndex + rows.length}
|
||||
</strong>{' '}
|
||||
{t('of')} <strong>{count}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div >
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<div className='form-actions-container'>
|
||||
<div className='btn-group'>
|
||||
{bulkActions.length > 0 && (
|
||||
<DropdownButton
|
||||
id='bulk-actions'
|
||||
bsSize='small'
|
||||
bsStyle='default'
|
||||
noCaret={true}
|
||||
title={(
|
||||
<>
|
||||
{t('Actions')} <span className='caret' />
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{bulkActions.map((action) => (
|
||||
<MenuItem
|
||||
id={action.name}
|
||||
key={action.key || action.name}
|
||||
eventKey={selectedFlatRows}
|
||||
onSelect={
|
||||
(selectedRows: typeof selectedFlatRows) => {
|
||||
action.onSelect(selectedRows.map((r: any) => r.original));
|
||||
}
|
||||
}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={8} className='text-center'>
|
||||
<Pagination
|
||||
prev={canPreviousPage}
|
||||
first={pageIndex > 1}
|
||||
next={canNextPage}
|
||||
last={pageIndex < pageCount - 2}
|
||||
items={pageCount}
|
||||
activePage={pageIndex + 1}
|
||||
ellipsis={true}
|
||||
boundaryLinks={true}
|
||||
maxButtons={5}
|
||||
onSelect={(p: number) => gotoPage(p - 1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={2}>
|
||||
<span className='pull-right'>
|
||||
{t('showing')}{' '}
|
||||
<strong>
|
||||
{pageSize * pageIndex + (rows.length && 1)}-{pageSize * pageIndex + rows.length}
|
||||
</strong>{' '}
|
||||
{t('of')} <strong>{count}</strong>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</div >
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import { Cell, HeaderGroup, Row } from 'react-table';
|
||||
|
||||
interface Props<D> {
|
||||
interface Props<D extends object = {}> {
|
||||
getTableProps: (userProps?: any) => any;
|
||||
getTableBodyProps: (userProps?: any) => any;
|
||||
prepareRow: (row: Row<D>) => any;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<FilterToggle[]>(
|
||||
|
@ -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 },
|
||||
|
|
|
@ -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<D extends object>
|
||||
extends UseExpandedOptions<D>,
|
||||
UseFiltersOptions<D>,
|
||||
UseFiltersOptions<D>,
|
||||
UseGlobalFiltersOptions<D>,
|
||||
UseGroupByOptions<D>,
|
||||
UsePaginationOptions<D>,
|
||||
UseResizeColumnsOptions<D>,
|
||||
UseRowSelectOptions<D>,
|
||||
UseRowStateOptions<D>,
|
||||
UseSortByOptions<D>,
|
||||
// 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<string, any> {}
|
||||
|
||||
export interface Hooks<D extends object = {}>
|
||||
extends UseExpandedHooks<D>,
|
||||
UseGroupByHooks<D>,
|
||||
UseRowSelectHooks<D>,
|
||||
UseSortByHooks<D> {}
|
||||
|
||||
export interface TableInstance<D extends object = {}>
|
||||
extends UseColumnOrderInstanceProps<D>,
|
||||
UseExpandedInstanceProps<D>,
|
||||
UseFiltersInstanceProps<D>,
|
||||
UseGlobalFiltersInstanceProps<D>,
|
||||
UseGroupByInstanceProps<D>,
|
||||
UsePaginationInstanceProps<D>,
|
||||
UseRowSelectInstanceProps<D>,
|
||||
UseRowStateInstanceProps<D>,
|
||||
UseSortByInstanceProps<D> {}
|
||||
|
||||
export interface TableState<D extends object = {}>
|
||||
extends UseColumnOrderState<D>,
|
||||
UseExpandedState<D>,
|
||||
UseFiltersState<D>,
|
||||
UseGlobalFiltersState<D>,
|
||||
UseGroupByState<D>,
|
||||
UsePaginationState<D>,
|
||||
UseResizeColumnsState<D>,
|
||||
UseRowSelectState<D>,
|
||||
UseRowStateState<D>,
|
||||
UseSortByState<D> {}
|
||||
|
||||
export interface Column<D extends object = {}>
|
||||
extends UseFiltersColumnOptions<D>,
|
||||
UseGroupByColumnOptions<D>,
|
||||
UseResizeColumnsColumnOptions<D>,
|
||||
UseSortByColumnOptions<D> {
|
||||
cellProps?: any;
|
||||
}
|
||||
|
||||
export interface ColumnInstance<D extends object = {}>
|
||||
extends UseFiltersColumnProps<D>,
|
||||
UseGroupByColumnProps<D>,
|
||||
UseResizeColumnsColumnProps<D>,
|
||||
UseSortByColumnProps<D> {}
|
||||
|
||||
export interface Cell<D extends object = {}>
|
||||
extends UseGroupByCellProps<D>,
|
||||
UseRowStateCellProps<D> {}
|
||||
|
||||
export interface Row<D extends object = {}>
|
||||
extends UseExpandedRowProps<D>,
|
||||
UseGroupByRowProps<D>,
|
||||
UseRowSelectRowProps<D>,
|
||||
UseRowStateRowProps<D> {}
|
||||
}
|
|
@ -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 <https://github.com/grsmto>
|
||||
// Artem Berdyshev <https://github.com/berdyshev>
|
||||
// Christian Murphy <https://github.com/ChristianMurphy>
|
||||
// Tai Dupreee <https://github.com/nytai>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 3.0
|
||||
declare module 'react-table' {
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
|
||||
export interface Cell<D> {
|
||||
render: (type: string) => any;
|
||||
getCellProps: () => { key: string; [k: string]: any };
|
||||
column: Column<D>;
|
||||
row: Row<D>;
|
||||
state: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface Row<D> {
|
||||
index: number;
|
||||
cells: Array<Cell<D>>;
|
||||
getRowProps: () => { key: string; [k: string]: any };
|
||||
original: any;
|
||||
state?: any;
|
||||
setState?: (state: any) => any;
|
||||
}
|
||||
|
||||
export interface HeaderColumn<D, A extends keyof D = never> {
|
||||
/**
|
||||
* This string/function is used to build the data model for your column.
|
||||
*/
|
||||
accessor: A | ((originalRow: D) => string);
|
||||
Header?: string | ((props: TableInstance<D>) => ReactNode);
|
||||
Filter?: string | ((props: TableInstance<D>) => ReactNode);
|
||||
Cell?: string | ((cell: Cell<D>) => 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<D, A extends keyof D = never>
|
||||
extends HeaderColumn<D, A> {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
export type Page<D> = Array<Row<D>>;
|
||||
|
||||
export interface EnhancedColumn<D, A extends keyof D = never>
|
||||
extends Column<D, A> {
|
||||
render: (type: string) => any;
|
||||
getHeaderProps: (userProps?: any) => any;
|
||||
getSortByToggleProps: (userProps?: any) => any;
|
||||
sorted: boolean;
|
||||
sortedDesc: boolean;
|
||||
sortedIndex: number;
|
||||
}
|
||||
|
||||
export interface HeaderGroup<D, A extends keyof D = never> {
|
||||
headers: Array<EnhancedColumn<D, A>>;
|
||||
getRowProps: (userProps?: any) => any;
|
||||
getHeaderGroupProps: (userProps?: any) => any;
|
||||
}
|
||||
|
||||
export interface Hooks<D> {
|
||||
beforeRender: [];
|
||||
columns: [];
|
||||
headerGroups: [];
|
||||
headers: [];
|
||||
rows: Array<Row<D>>;
|
||||
row: [];
|
||||
renderableRows: [];
|
||||
getTableProps: [];
|
||||
getRowProps: [];
|
||||
getHeaderRowProps: [];
|
||||
getHeaderProps: [];
|
||||
getCellProps: [];
|
||||
}
|
||||
|
||||
export interface TableInstance<D>
|
||||
extends TableOptions<D>,
|
||||
UseRowsValues<D>,
|
||||
UseFiltersValues,
|
||||
UsePaginationValues<D>,
|
||||
UseColumnsValues<D>,
|
||||
UseRowStateValues<D> {
|
||||
hooks: Hooks<D>;
|
||||
rows: Array<Row<D>>;
|
||||
columns: Array<EnhancedColumn<D>>;
|
||||
getTableProps: (userProps?: any) => any;
|
||||
getTableBodyProps: (userProps?: any) => any;
|
||||
getRowProps: (userProps?: any) => any;
|
||||
prepareRow: (row: Row<D>) => any;
|
||||
getSelectRowToggleProps: (userProps?: any) => any;
|
||||
toggleSelectAll: (forcedState: boolean) => any;
|
||||
state: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface TableOptions<D> {
|
||||
data: D[];
|
||||
columns: Array<HeaderColumn<D>>;
|
||||
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<D> {
|
||||
nextPage: () => any;
|
||||
previousPage: () => any;
|
||||
setPageSize: (size: number) => any;
|
||||
gotoPage: (page: number) => any;
|
||||
canPreviousPage: boolean;
|
||||
canNextPage: boolean;
|
||||
page: Page<D>;
|
||||
pageOptions: [];
|
||||
}
|
||||
|
||||
export interface UseRowsValues<D> {
|
||||
rows: Array<Row<D>>;
|
||||
}
|
||||
|
||||
export interface UseColumnsValues<D> {
|
||||
columns: Array<EnhancedColumn<D>>;
|
||||
headerGroups: Array<HeaderGroup<D>>;
|
||||
headers: Array<EnhancedColumn<D>>;
|
||||
}
|
||||
|
||||
export interface UseFiltersValues {
|
||||
setFilter: (columnId: string, filter: string) => any;
|
||||
setAllFilters: (filterObj: any) => any;
|
||||
}
|
||||
|
||||
export interface UseRowStateValues<D> {
|
||||
setRowState: (rowPath: string[], updater: (state: any) => any) => any;
|
||||
}
|
||||
|
||||
export function useTable<D>(
|
||||
props: TableOptions<D>,
|
||||
...plugins: any[]
|
||||
): TableInstance<D>;
|
||||
|
||||
export function useColumns<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & UseColumnsValues<D>;
|
||||
|
||||
export function useRows<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & UseRowsValues<D>;
|
||||
|
||||
export function useFilters<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & {
|
||||
rows: Array<Row<D>>;
|
||||
};
|
||||
|
||||
export function useSortBy<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & {
|
||||
rows: Array<Row<D>>;
|
||||
};
|
||||
|
||||
export function useGroupBy<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & { rows: Array<Row<D>> };
|
||||
|
||||
export function usePagination<D>(
|
||||
props: TableOptions<D>,
|
||||
): UsePaginationValues<D>;
|
||||
|
||||
export function useRowState<D>(props: TableOptions<D>): UseRowStateValues<D>;
|
||||
|
||||
export function useFlexLayout<D>(props: TableOptions<D>): TableOptions<D>;
|
||||
|
||||
export function useExpanded<D>(
|
||||
props: TableOptions<D>,
|
||||
): TableOptions<D> & {
|
||||
toggleExpandedByPath: () => any;
|
||||
expandedDepth: [];
|
||||
rows: [];
|
||||
};
|
||||
|
||||
export function useTableState(
|
||||
initialState?: any,
|
||||
overriddenState?: any,
|
||||
options?: {
|
||||
reducer?: (oldState: any, newState: any, type: string) => any;
|
||||
useState?: [any, Dispatch<SetStateAction<any>>];
|
||||
},
|
||||
): any;
|
||||
|
||||
export const actions: any;
|
||||
}
|
|
@ -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<Props, State> {
|
||||
|
||||
get canEdit() {
|
||||
|
@ -56,6 +68,10 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
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<Props, State> {
|
|||
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) => <a href={url}>{dashboard_title}</a>,
|
||||
Header: this.state.labelColumns.dashboard_title || '',
|
||||
accessor: 'dashboard_title',
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { changed_by_name, changed_by_url },
|
||||
},
|
||||
}: any) => <a href={changed_by_url}>{changed_by_name}</a>,
|
||||
Header: this.state.labelColumns.changed_by_name || '',
|
||||
accessor: 'changed_by_fk',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { published },
|
||||
},
|
||||
}: any) => (
|
||||
<span className='no-wrap'>{published ? <i className='fa fa-check' /> : ''}</span>
|
||||
),
|
||||
Header: this.state.labelColumns.published || '',
|
||||
accessor: 'published',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { changed_on },
|
||||
},
|
||||
}: any) => (
|
||||
<span className='no-wrap'>{moment(changed_on).fromNow()}</span>
|
||||
),
|
||||
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 (
|
||||
<span className={`actions ${state && state.hover ? '' : 'invisible'}`}>
|
||||
{this.canDelete && (
|
||||
<span
|
||||
role='button'
|
||||
className='action-button'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<i className='fa fa-trash' />
|
||||
</span>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<span
|
||||
role='button'
|
||||
className='action-button'
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<i className='fa fa-pencil' />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
public columns = [
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { url, dashboard_title },
|
||||
},
|
||||
Header: 'Actions',
|
||||
id: 'actions',
|
||||
}: any) => <a href={url}>{dashboard_title}</a>,
|
||||
Header: t('Title'),
|
||||
accessor: 'dashboard_title',
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { changed_by_name, changed_by_url },
|
||||
},
|
||||
}: any) => <a href={changed_by_url}>{changed_by_name}</a>,
|
||||
Header: t('Changed By Name'),
|
||||
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: ({
|
||||
row: {
|
||||
original: { changed_on },
|
||||
},
|
||||
}: any) => (
|
||||
<span className='no-wrap'>{moment(changed_on).fromNow()}</span>
|
||||
),
|
||||
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 (
|
||||
<span className={`actions ${state && state.hover ? '' : 'invisible'}`}>
|
||||
{this.canDelete && (
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={<>{t('Are you sure you want to delete')} <b>{original.dashboard_title}</b>?</>}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{(confirmDelete) => (
|
||||
<span
|
||||
role='button'
|
||||
className='action-button'
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<i className='fa fa-trash' />
|
||||
</span>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
)}
|
||||
{this.canExport && (
|
||||
<span
|
||||
role='button'
|
||||
className='action-button'
|
||||
onClick={handleExport}
|
||||
>
|
||||
<i className='fa fa-database' />
|
||||
</span>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<span
|
||||
role='button'
|
||||
className='action-button'
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<i className='fa fa-pencil' />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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<Props, State> {
|
|||
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<Props, State> {
|
|||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setColumns();
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
|
@ -257,38 +289,58 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
public render() {
|
||||
const { dashboards, dashboardCount, loading, filterTypes } = this.state;
|
||||
return (
|
||||
<div className='container welcome'>
|
||||
<div className='container welcome' >
|
||||
<Panel>
|
||||
<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}
|
||||
filterTypes={filterTypes}
|
||||
/>
|
||||
<ConfirmStatusChange
|
||||
title={t('Please confirm')}
|
||||
description={t('Are you sure you want to delete the selected dashboards?')}
|
||||
onConfirm={this.handleBulkDashboardDelete}
|
||||
>
|
||||
{(confirmDelete) => {
|
||||
const bulkActions = [];
|
||||
if (this.canDelete) {
|
||||
bulkActions.push({
|
||||
key: 'delete',
|
||||
name: <><i className='fa fa-trash' /> Delete</>,
|
||||
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}
|
||||
filterTypes={filterTypes}
|
||||
bulkActions={bulkActions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
</Panel>
|
||||
|
||||
<Modal show={this.state.showDeleteModal} onHide={this.toggleModal}>
|
||||
<Modal.Header closeButton={true} />
|
||||
<Modal.Body>
|
||||
{t('Are you sure you want to delete')}{' '}
|
||||
<b>{this.state.deleteCandidate.dashboard_title}</b>?
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.toggleModal}>{t('Cancel')}</Button>
|
||||
<Button bsStyle='danger' onClick={this.handleDashboardDelete}>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private hasPerm = (perm: string) => {
|
||||
if (!this.state.permissions.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(this.state.permissions.find((p) => p === perm));
|
||||
}
|
||||
}
|
||||
|
||||
export default withToasts(DashboardList);
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue