adds FAB style filter types (#9086)
This commit is contained in:
parent
72518e20ee
commit
1748728c45
|
|
@ -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": "*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Props> = ({
|
|||
initialSort = [],
|
||||
className = '',
|
||||
title = '',
|
||||
filterTypes = {},
|
||||
filters = [],
|
||||
bulkActions = [],
|
||||
}) => {
|
||||
const {
|
||||
|
|
@ -96,12 +105,12 @@ const ListView: FunctionComponent<Props> = ({
|
|||
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<Props> = ({
|
|||
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<Props> = ({
|
|||
</>
|
||||
}
|
||||
>
|
||||
{filterableColumns
|
||||
.map(({ id, accessor, Header }) => ({
|
||||
{filters
|
||||
.map(({ id, Header }) => ({
|
||||
Header,
|
||||
id: id || accessor,
|
||||
id,
|
||||
}))
|
||||
.map((ft: FilterToggle) => (
|
||||
.map((ft: InternalFilter) => (
|
||||
<MenuItem
|
||||
key={ft.id}
|
||||
eventKey={ft}
|
||||
onSelect={(fltr: FilterToggle) =>
|
||||
setFilterToggles([...filterToggles, fltr])
|
||||
onSelect={(fltr: InternalFilter) =>
|
||||
setInternalFilters([...internalFilters, fltr])
|
||||
}
|
||||
>
|
||||
{ft.Header}
|
||||
|
|
@ -169,62 +175,89 @@ const ListView: FunctionComponent<Props> = ({
|
|||
)}
|
||||
</Row>
|
||||
<hr />
|
||||
{filterToggles.map((ft, i) => (
|
||||
<div key={`${ft.Header}-${i}`} className="filter-inputs">
|
||||
<Row>
|
||||
<Col className="text-center filter-column" md={2}>
|
||||
<span>{ft.Header}</span>
|
||||
</Col>
|
||||
<Col md={2}>
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
bsSize="small"
|
||||
value={ft.filterId}
|
||||
placeholder={
|
||||
filterTypes[ft.id] ? filterTypes[ft.id][0].name : ''
|
||||
}
|
||||
onChange={(e: React.MouseEvent<HTMLInputElement>) =>
|
||||
updateFilterToggle(i, { filterId: e.currentTarget.value })
|
||||
}
|
||||
>
|
||||
{filterTypes[ft.id] &&
|
||||
filterTypes[ft.id].map(
|
||||
({ name, operator }: FilterType) => (
|
||||
<option key={name} value={operator}>
|
||||
{name}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</FormControl>
|
||||
</Col>
|
||||
<Col md={1} />
|
||||
<Col md={4}>
|
||||
<FormControl
|
||||
type="text"
|
||||
bsSize="small"
|
||||
value={ft.value || ''}
|
||||
onChange={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
updateFilterToggle(i, {
|
||||
value: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={1}>
|
||||
<div
|
||||
className="filter-close"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => removeFilterAndApply(i)}
|
||||
>
|
||||
<i className="fa fa-close text-primary" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</div>
|
||||
))}
|
||||
{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 (
|
||||
<div key={`${ft.Header}-${i}`} className="filter-inputs">
|
||||
<Row>
|
||||
<Col className="text-center filter-column" md={2}>
|
||||
<span>{ft.Header}</span>
|
||||
</Col>
|
||||
<Col md={2}>
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
bsSize="small"
|
||||
value={ft.operator}
|
||||
placeholder={filter ? filter.operators[0] : ''}
|
||||
onChange={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||
updateInternalFilter(i, {
|
||||
operator: e.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{filter.operators.map(({ label, value }: Select) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</FormControl>
|
||||
</Col>
|
||||
<Col md={1} />
|
||||
<Col md={4}>
|
||||
{filter.input === 'select' && (
|
||||
<VirtualizedSelect
|
||||
autoFocus
|
||||
multi
|
||||
searchable
|
||||
name={`filter-${filter.id}-select`}
|
||||
options={filter.selects}
|
||||
placeholder="Select Value"
|
||||
value={ft.value}
|
||||
selectComponent={SelectComponent}
|
||||
onChange={(e: Select[] | null) => {
|
||||
updateInternalFilter(i, {
|
||||
operator: ft.operator || filter.operators[0].value,
|
||||
value: e ? e.map(s => s.value) : e,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{filter.input !== 'select' && (
|
||||
<FormControl
|
||||
type={filter.input ? filter.input : 'text'}
|
||||
bsSize="small"
|
||||
value={ft.value || ''}
|
||||
checked={Boolean(ft.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.persist();
|
||||
updateInternalFilter(i, {
|
||||
operator: ft.operator || filter.operators[0].value,
|
||||
value: extractInputValue(filter.input, e),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col md={1}>
|
||||
<div
|
||||
className="filter-close"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => removeFilterAndApply(i)}
|
||||
>
|
||||
<i className="fa fa-close text-primary" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{internalFilters.length > 0 && (
|
||||
<>
|
||||
<Row>
|
||||
<Col md={10} />
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FilterToggle[]>(
|
||||
const [internalFilters, setInternalFilters] = useState<InternalFilter[]>(
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Props, State> {
|
|||
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<Props, State> {
|
|||
}: any) => <a href={url}>{sliceName}</a>,
|
||||
Header: t('Chart'),
|
||||
accessor: 'slice_name',
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -274,11 +299,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
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<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="container welcome">
|
||||
<Panel>
|
||||
|
|
@ -324,7 +389,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
fetchData={this.fetchData}
|
||||
loading={loading}
|
||||
initialSort={this.initialSort}
|
||||
filterTypes={filterTypes}
|
||||
filters={filters}
|
||||
bulkActions={bulkActions}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Props, State> {
|
|||
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<Props, State> {
|
|||
}: any) => <a href={url}>{dashboardTitle}</a>,
|
||||
Header: t('Title'),
|
||||
accessor: 'dashboard_title',
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -117,7 +144,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
},
|
||||
},
|
||||
}: any) => <a href={changedByUrl}>{changedByName}</a>,
|
||||
Header: t('Changed By Name'),
|
||||
Header: t('Creator'),
|
||||
accessor: 'changed_by_fk',
|
||||
sortable: true,
|
||||
},
|
||||
|
|
@ -141,10 +168,16 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
original: { changed_on: changedOn },
|
||||
},
|
||||
}: any) => <span className="no-wrap">{moment(changedOn).fromNow()}</span>,
|
||||
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<Props, State> {
|
|||
};
|
||||
|
||||
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<Props, State> {
|
|||
},
|
||||
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<Props, State> {
|
|||
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<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="container welcome">
|
||||
<Panel>
|
||||
|
|
@ -362,7 +433,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
fetchData={this.fetchData}
|
||||
loading={loading}
|
||||
initialSort={this.initialSort}
|
||||
filterTypes={filterTypes}
|
||||
filters={filters}
|
||||
bulkActions={bulkActions}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue