mirror of https://github.com/apache/superset.git
[Dashboard] new listview filters & emotion infra (#9462)
* [Dashboard] listview filters to match new design * use title_or_slug filter * use ?. operator * move components to components folder
This commit is contained in:
parent
a797465ae5
commit
f90824fa17
|
@ -34,13 +34,10 @@ module.exports = {
|
|||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
babelConfig: true,
|
||||
diagnostics: {
|
||||
warnOnly: true,
|
||||
},
|
||||
tsConfig: {
|
||||
jsx: 'react',
|
||||
esModuleInterop: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -16,7 +16,7 @@
|
|||
"prod": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --mode=production --colors --progress",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --colors --progress",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production webpack --mode=production --colors --progress",
|
||||
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx . && prettier --check '{src,stylesheets}/**/*.{css,less,sass,scss}'",
|
||||
"lint": "prettier --check '{src,stylesheets}/**/*.{css,less,sass,scss}' && eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx .",
|
||||
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx . && npm run clean-css",
|
||||
"clean-css": "prettier --write '{src,stylesheets}/**/*.{css,less,sass,scss}'"
|
||||
},
|
||||
|
@ -53,6 +53,8 @@
|
|||
"dependencies": {
|
||||
"@babel/runtime-corejs3": "^7.8.4",
|
||||
"@data-ui/sparkline": "^0.0.54",
|
||||
"@emotion/core": "^10.0.28",
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@superset-ui/chart": "^0.12.11",
|
||||
"@superset-ui/chart-composition": "^0.12.8",
|
||||
"@superset-ui/color": "^0.12.8",
|
||||
|
@ -108,6 +110,7 @@
|
|||
"d3-scale": "^2.1.2",
|
||||
"dnd-core": "^2.6.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"emotion-theming": "^10.0.27",
|
||||
"geolib": "^2.0.24",
|
||||
"immutable": "^3.8.2",
|
||||
"interweave": "^11.2.0",
|
||||
|
@ -147,7 +150,7 @@
|
|||
"react-split": "^2.0.4",
|
||||
"react-sticky": "^6.0.2",
|
||||
"react-syntax-highlighter": "^7.0.4",
|
||||
"react-table": "^7.0.0-rc.15",
|
||||
"react-table": "^7.0.4",
|
||||
"react-transition-group": "^2.5.3",
|
||||
"react-ultimate-pagination": "^1.2.0",
|
||||
"react-virtualized": "9.19.1",
|
||||
|
@ -175,10 +178,12 @@
|
|||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/register": "^7.8.6",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/jquery": "^3.3.32",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-json-tree": "^0.6.11",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-table": "^7.0.2",
|
||||
"@types/react-ultimate-pagination": "^1.2.0",
|
||||
|
@ -189,12 +194,14 @@
|
|||
"babel-jest": "^25.1.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||
"babel-plugin-emotion": "^10.0.29",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"cache-loader": "^1.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"emotion-ts-plugin": "^0.5.3",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"eslint": "^6.2.2",
|
||||
|
|
|
@ -20,43 +20,45 @@ import React from 'react';
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MenuItem, Pagination } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListViewFilters from 'src/components/ListView/Filters';
|
||||
import ListViewPagination from 'src/components/ListView/Pagination';
|
||||
import { areArraysShallowEqual } from 'src/reduxUtils';
|
||||
|
||||
const mockedProps = {
|
||||
title: 'Data Table',
|
||||
columns: [
|
||||
{
|
||||
accessor: 'id',
|
||||
Header: 'ID',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
accessor: 'name',
|
||||
Header: 'Name',
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
Header: 'Name',
|
||||
id: 'name',
|
||||
operators: [{ label: 'Starts With', value: 'sw' }],
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{ id: 1, name: 'data 1' },
|
||||
{ id: 2, name: 'data 2' },
|
||||
],
|
||||
count: 2,
|
||||
pageSize: 1,
|
||||
fetchData: jest.fn(() => []),
|
||||
loading: false,
|
||||
bulkActions: [{ name: 'do something', onSelect: jest.fn() }],
|
||||
};
|
||||
|
||||
describe('ListView', () => {
|
||||
const mockedProps = {
|
||||
title: 'Data Table',
|
||||
columns: [
|
||||
{
|
||||
accessor: 'id',
|
||||
Header: 'ID',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
accessor: 'name',
|
||||
Header: 'Name',
|
||||
filterable: true,
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
Header: 'Name',
|
||||
id: 'name',
|
||||
operators: [{ label: 'Starts With', value: 'sw' }],
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{ id: 1, name: 'data 1' },
|
||||
{ id: 2, name: 'data 2' },
|
||||
],
|
||||
count: 2,
|
||||
pageSize: 1,
|
||||
fetchData: jest.fn(() => []),
|
||||
loading: false,
|
||||
bulkActions: [{ name: 'do something', onSelect: jest.fn() }],
|
||||
};
|
||||
const wrapper = mount(<ListView {...mockedProps} />);
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -138,34 +140,35 @@ describe('ListView', () => {
|
|||
wrapper.update();
|
||||
|
||||
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"Header": "name",
|
||||
"id": "name",
|
||||
"operator": "sw",
|
||||
"value": "foo",
|
||||
},
|
||||
],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"id": "name",
|
||||
"operator": "sw",
|
||||
"value": "foo",
|
||||
},
|
||||
],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders pagination controls', () => {
|
||||
expect(wrapper.find(Pagination).exists()).toBe(true);
|
||||
expect(wrapper.find(Pagination.Prev).exists()).toBe(true);
|
||||
expect(wrapper.find(Pagination.Item).exists()).toBe(true);
|
||||
expect(wrapper.find(Pagination.Next).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls fetchData on page change', () => {
|
||||
act(() => {
|
||||
wrapper.find(ListViewPagination).prop('onChange')(2);
|
||||
|
@ -173,28 +176,28 @@ describe('ListView', () => {
|
|||
wrapper.update();
|
||||
|
||||
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"Header": "name",
|
||||
"id": "name",
|
||||
"operator": "sw",
|
||||
"value": "foo",
|
||||
},
|
||||
],
|
||||
"pageIndex": 1,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [
|
||||
Object {
|
||||
"desc": false,
|
||||
"id": "id",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"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(() => {
|
||||
wrapper
|
||||
|
@ -228,6 +231,7 @@ describe('ListView', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles bulk actions on all rows', () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
|
@ -265,6 +269,7 @@ describe('ListView', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('Throws an exception if filter missing in columns', () => {
|
||||
expect.assertions(1);
|
||||
const props = {
|
||||
|
@ -280,3 +285,105 @@ describe('ListView', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListView with new UI filters', () => {
|
||||
const newFiltersProps = {
|
||||
...mockedProps,
|
||||
useNewUIFilters: true,
|
||||
filters: [
|
||||
{
|
||||
Header: 'ID',
|
||||
id: 'id',
|
||||
input: 'select',
|
||||
selects: [{ label: 'foo', value: 'bar' }],
|
||||
operator: 'eq',
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
id: 'name',
|
||||
input: 'search',
|
||||
operator: 'ct',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const wrapper = mount(<ListView {...newFiltersProps} />);
|
||||
|
||||
afterEach(() => {
|
||||
mockedProps.fetchData.mockClear();
|
||||
mockedProps.bulkActions.forEach(ba => {
|
||||
ba.onSelect.mockClear();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders UI filters', () => {
|
||||
expect(wrapper.find(ListViewFilters)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('calls fetchData on filter', () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[data-test="filters-select"]')
|
||||
.last()
|
||||
.props()
|
||||
.onChange({ value: 'bar' });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[data-test="filters-search"]')
|
||||
.last()
|
||||
.props()
|
||||
.onChange({ currentTarget: { value: 'something' } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[data-test="filters-search"]')
|
||||
.last()
|
||||
.props()
|
||||
.onBlur();
|
||||
});
|
||||
|
||||
expect(newFiltersProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"id": "id",
|
||||
"operator": "eq",
|
||||
"value": "bar",
|
||||
},
|
||||
],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(newFiltersProps.fetchData.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"id": "id",
|
||||
"operator": "eq",
|
||||
"value": "bar",
|
||||
},
|
||||
Object {
|
||||
"id": "name",
|
||||
"operator": "ct",
|
||||
"value": "something",
|
||||
},
|
||||
],
|
||||
"pageIndex": 0,
|
||||
"pageSize": 1,
|
||||
"sortBy": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* 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, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { withTheme } from 'emotion-theming';
|
||||
|
||||
import StyledSelect from 'src/components/StyledSelect';
|
||||
import SearchInput from 'src/components/SearchInput';
|
||||
import { Filter, Filters, FilterValue, InternalFilter } from './types';
|
||||
|
||||
interface BaseFilter {
|
||||
Header: string;
|
||||
initialValue: any;
|
||||
}
|
||||
interface SelectFilterProps extends BaseFilter {
|
||||
onSelect: (selected: any) => any;
|
||||
selects: Filter['selects'];
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
const FilterContainer = styled.div`
|
||||
display: inline;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
|
||||
|
||||
function SelectFilter({
|
||||
Header,
|
||||
selects = [],
|
||||
emptyLabel = 'None',
|
||||
initialValue,
|
||||
onSelect,
|
||||
}: SelectFilterProps) {
|
||||
const clearFilterSelect = {
|
||||
label: emptyLabel,
|
||||
value: CLEAR_SELECT_FILTER_VALUE,
|
||||
};
|
||||
const options = React.useMemo(() => [clearFilterSelect, ...selects], [
|
||||
emptyLabel,
|
||||
selects,
|
||||
]);
|
||||
|
||||
const [value, setValue] = useState(
|
||||
typeof initialValue === 'undefined'
|
||||
? clearFilterSelect.value
|
||||
: initialValue,
|
||||
);
|
||||
const onChange = (selected: { label: string; value: any } | null) => {
|
||||
if (selected === null) return;
|
||||
setValue(selected.value);
|
||||
onSelect(
|
||||
selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterContainer>
|
||||
<Title>{Header}:</Title>
|
||||
<StyledSelect
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
clearable={false}
|
||||
/>
|
||||
</FilterContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface SearchHeaderProps extends BaseFilter {
|
||||
Header: string;
|
||||
onSubmit: (val: string) => void;
|
||||
}
|
||||
|
||||
function SearchFilter({ Header, initialValue, onSubmit }: SearchHeaderProps) {
|
||||
const [value, setValue] = useState(initialValue || '');
|
||||
const handleSubmit = () => onSubmit(value);
|
||||
|
||||
return (
|
||||
<FilterContainer>
|
||||
<SearchInput
|
||||
data-test="filters-search"
|
||||
placeholder={Header}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
setValue(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
onBlur={handleSubmit}
|
||||
/>
|
||||
</FilterContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface UIFiltersProps {
|
||||
filters: Filters;
|
||||
internalFilters: InternalFilter[];
|
||||
updateFilterValue: (id: number, value: FilterValue['value']) => void;
|
||||
}
|
||||
|
||||
const FilterWrapper = styled.div`
|
||||
padding: 24px 16px 8px;
|
||||
`;
|
||||
|
||||
function UIFilters({
|
||||
filters,
|
||||
internalFilters = [],
|
||||
updateFilterValue,
|
||||
}: UIFiltersProps) {
|
||||
return (
|
||||
<FilterWrapper>
|
||||
{filters.map(({ Header, input, selects, unfilteredLabel }, index) => {
|
||||
const initialValue =
|
||||
internalFilters[index] && internalFilters[index].value;
|
||||
if (input === 'select') {
|
||||
return (
|
||||
<SelectFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
selects={selects}
|
||||
emptyLabel={unfilteredLabel}
|
||||
initialValue={initialValue}
|
||||
onSelect={(value: any) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input === 'search') {
|
||||
return (
|
||||
<SearchFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
initialValue={initialValue}
|
||||
onSubmit={(value: string) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</FilterWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(UIFilters);
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* 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 React, { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DropdownButton,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Row,
|
||||
// @ts-ignore
|
||||
} from 'react-bootstrap';
|
||||
// @ts-ignore
|
||||
import SelectComponent from 'react-select';
|
||||
// @ts-ignore
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { Filters, InternalFilter, Select } from './types';
|
||||
import { extractInputValue, getDefaultFilterOperator } from './utils';
|
||||
|
||||
export const FilterMenu = ({
|
||||
filters,
|
||||
internalFilters,
|
||||
setInternalFilters,
|
||||
}: {
|
||||
filters: Filters;
|
||||
internalFilters: InternalFilter[];
|
||||
setInternalFilters: Dispatch<SetStateAction<InternalFilter[]>>;
|
||||
}) => (
|
||||
<div className="filter-dropdown">
|
||||
<DropdownButton
|
||||
id="filter-picker"
|
||||
bsSize="small"
|
||||
bsStyle={'default'}
|
||||
noCaret
|
||||
title={
|
||||
<>
|
||||
<i className="fa fa-filter text-primary" />
|
||||
{' '}
|
||||
{t('Filter List')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{filters
|
||||
.map(({ id, Header }) => ({
|
||||
Header,
|
||||
id,
|
||||
value: undefined,
|
||||
}))
|
||||
.map(ft => (
|
||||
<MenuItem
|
||||
key={ft.id}
|
||||
eventKey={ft}
|
||||
onSelect={(fltr: typeof ft) =>
|
||||
setInternalFilters([...internalFilters, fltr])
|
||||
}
|
||||
>
|
||||
{ft.Header}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FilterInputs = ({
|
||||
internalFilters,
|
||||
filters,
|
||||
updateInternalFilter,
|
||||
removeFilterAndApply,
|
||||
filtersApplied,
|
||||
applyFilters,
|
||||
}: {
|
||||
internalFilters: InternalFilter[];
|
||||
filters: Filters;
|
||||
updateInternalFilter: (i: number, f: object) => void;
|
||||
removeFilterAndApply: (i: number) => void;
|
||||
filtersApplied: boolean;
|
||||
applyFilters: () => void;
|
||||
}) => (
|
||||
<>
|
||||
{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 ? getDefaultFilterOperator(filter) : ''}
|
||||
onChange={(e: React.ChangeEvent<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 || getDefaultFilterOperator(filter),
|
||||
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 || getDefaultFilterOperator(filter),
|
||||
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} />
|
||||
<Col md={2}>
|
||||
<Button
|
||||
data-test="apply-filters"
|
||||
disabled={!!filtersApplied}
|
||||
bsStyle="primary"
|
||||
onClick={applyFilters}
|
||||
bsSize="small"
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
|
@ -19,10 +19,8 @@
|
|||
import { t } from '@superset-ui/translation';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DropdownButton,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Row,
|
||||
// @ts-ignore
|
||||
|
@ -34,20 +32,10 @@ import VirtualizedSelect from 'react-virtualized-select';
|
|||
import IndeterminateCheckbox from '../IndeterminateCheckbox';
|
||||
import TableCollection from './TableCollection';
|
||||
import Pagination from './Pagination';
|
||||
import {
|
||||
FetchDataConfig,
|
||||
Filters,
|
||||
InternalFilter,
|
||||
Select,
|
||||
SortColumn,
|
||||
} from './types';
|
||||
import {
|
||||
convertFilters,
|
||||
extractInputValue,
|
||||
ListViewError,
|
||||
removeFromList,
|
||||
useListViewState,
|
||||
} from './utils';
|
||||
import { FilterMenu, FilterInputs } from './LegacyFilters';
|
||||
import FilterControls from './Filters';
|
||||
import { FetchDataConfig, Filters, SortColumn } from './types';
|
||||
import { ListViewError, useListViewState } from './utils';
|
||||
|
||||
import './ListViewStyles.less';
|
||||
|
||||
|
@ -67,6 +55,7 @@ interface Props {
|
|||
name: React.ReactNode;
|
||||
onSelect: (rows: any[]) => any;
|
||||
}>;
|
||||
useNewUIFilters?: boolean;
|
||||
}
|
||||
|
||||
const bulkSelectColumnConfig = {
|
||||
|
@ -95,6 +84,7 @@ const ListView: FunctionComponent<Props> = ({
|
|||
title = '',
|
||||
filters = [],
|
||||
bulkActions = [],
|
||||
useNewUIFilters = false,
|
||||
}) => {
|
||||
const {
|
||||
getTableProps,
|
||||
|
@ -104,9 +94,10 @@ const ListView: FunctionComponent<Props> = ({
|
|||
prepareRow,
|
||||
pageCount = 1,
|
||||
gotoPage,
|
||||
setAllFilters,
|
||||
removeFilterAndApply,
|
||||
setInternalFilters,
|
||||
updateInternalFilter,
|
||||
applyFilterValue,
|
||||
applyFilters,
|
||||
filtersApplied,
|
||||
selectedFlatRows,
|
||||
|
@ -120,6 +111,7 @@ const ListView: FunctionComponent<Props> = ({
|
|||
fetchData,
|
||||
initialPageSize,
|
||||
initialSort,
|
||||
initialFilters: useNewUIFilters ? filters : [],
|
||||
});
|
||||
const filterable = Boolean(filters.length);
|
||||
if (filterable) {
|
||||
|
@ -136,161 +128,56 @@ const ListView: FunctionComponent<Props> = ({
|
|||
});
|
||||
}
|
||||
|
||||
const removeFilterAndApply = (index: number) => {
|
||||
const updated = removeFromList(internalFilters, index);
|
||||
setInternalFilters(updated);
|
||||
setAllFilters(convertFilters(updated));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`superset-list-view ${className}`}>
|
||||
{title && filterable && (
|
||||
<div className="header">
|
||||
<Row>
|
||||
<Col md={10}>
|
||||
<h2>{t(title)}</h2>
|
||||
</Col>
|
||||
{filterable && (
|
||||
<Col md={2}>
|
||||
<div className="filter-dropdown">
|
||||
<DropdownButton
|
||||
id="filter-picker"
|
||||
bsSize="small"
|
||||
bsStyle={'default'}
|
||||
noCaret
|
||||
title={
|
||||
<>
|
||||
<i className="fa fa-filter text-primary" />
|
||||
{' '}
|
||||
{t('Filter List')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{filters
|
||||
.map(({ id, Header }) => ({
|
||||
Header,
|
||||
id,
|
||||
}))
|
||||
.map((ft: InternalFilter) => (
|
||||
<MenuItem
|
||||
key={ft.id}
|
||||
eventKey={ft}
|
||||
onSelect={(fltr: InternalFilter) =>
|
||||
setInternalFilters([...internalFilters, fltr])
|
||||
}
|
||||
>
|
||||
{ft.Header}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<hr />
|
||||
{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">
|
||||
<div className="header">
|
||||
{!useNewUIFilters && (
|
||||
<>
|
||||
{title && filterable && (
|
||||
<>
|
||||
<Row>
|
||||
<Col className="text-center filter-column" md={2}>
|
||||
<span>{ft.Header}</span>
|
||||
<Col md={10}>
|
||||
<h2>{t(title)}</h2>
|
||||
</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,
|
||||
});
|
||||
}}
|
||||
{filterable && (
|
||||
<Col md={2}>
|
||||
<FilterMenu
|
||||
filters={filters}
|
||||
internalFilters={internalFilters}
|
||||
setInternalFilters={setInternalFilters}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{internalFilters.length > 0 && (
|
||||
<>
|
||||
<Row>
|
||||
<Col md={10} />
|
||||
<Col md={2}>
|
||||
<Button
|
||||
data-test="apply-filters"
|
||||
disabled={!!filtersApplied}
|
||||
bsStyle="primary"
|
||||
onClick={applyFilters}
|
||||
bsSize="small"
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
<FilterInputs
|
||||
internalFilters={internalFilters}
|
||||
filters={filters}
|
||||
updateInternalFilter={updateInternalFilter}
|
||||
removeFilterAndApply={removeFilterAndApply}
|
||||
filtersApplied={filtersApplied}
|
||||
applyFilters={applyFilters}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{useNewUIFilters && (
|
||||
<>
|
||||
<Row>
|
||||
<Col md={10}>
|
||||
<h2>{t(title)}</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<FilterControls
|
||||
filters={filters}
|
||||
internalFilters={internalFilters}
|
||||
updateFilterValue={applyFilterValue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="body">
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
|
|
|
@ -31,8 +31,10 @@ export interface Select {
|
|||
export interface Filter {
|
||||
Header: string;
|
||||
id: string;
|
||||
operators: Select[];
|
||||
input?: 'text' | 'textarea' | 'select' | 'checkbox';
|
||||
operators?: Select[];
|
||||
operator?: string;
|
||||
input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search';
|
||||
unfilteredLabel?: string;
|
||||
selects?: Select[];
|
||||
}
|
||||
|
||||
|
@ -41,7 +43,7 @@ export type Filters = Filter[];
|
|||
export interface FilterValue {
|
||||
id: string;
|
||||
operator?: string;
|
||||
value: string | boolean | number;
|
||||
value: string | boolean | number | null | undefined;
|
||||
}
|
||||
|
||||
export interface FetchDataConfig {
|
||||
|
@ -52,7 +54,7 @@ export interface FetchDataConfig {
|
|||
}
|
||||
|
||||
export interface InternalFilter extends FilterValue {
|
||||
Header: string;
|
||||
Header?: string;
|
||||
}
|
||||
|
||||
export interface FilterOperatorMap {
|
||||
|
|
|
@ -33,7 +33,13 @@ import {
|
|||
useQueryParams,
|
||||
} from 'use-query-params';
|
||||
|
||||
import { FetchDataConfig, InternalFilter, SortColumn } from './types';
|
||||
import {
|
||||
FetchDataConfig,
|
||||
Filter,
|
||||
FilterValue,
|
||||
InternalFilter,
|
||||
SortColumn,
|
||||
} from './types';
|
||||
|
||||
export class ListViewError extends Error {
|
||||
name = 'ListViewError';
|
||||
|
@ -55,17 +61,22 @@ function updateInList(list: any[], index: number, update: any): any[] {
|
|||
];
|
||||
}
|
||||
|
||||
// convert filters from UI objects to data objects
|
||||
export function convertFilters(fts: InternalFilter[]) {
|
||||
return fts
|
||||
.filter((ft: InternalFilter) => ft.value)
|
||||
.map(ft => ({ operator: ft.operator, ...ft }));
|
||||
function mergeCreateFilterValues(list: Filter[], updateList: FilterValue[]) {
|
||||
return list.map(({ id, operator }) => {
|
||||
const update = updateList.find(obj => obj.id === id);
|
||||
|
||||
return { id, operator, value: update?.value };
|
||||
});
|
||||
}
|
||||
|
||||
export function extractInputValue(
|
||||
inputType: 'text' | 'textarea' | 'checkbox' | 'select' | undefined,
|
||||
event: any,
|
||||
) {
|
||||
// convert filters from UI objects to data objects
|
||||
export function convertFilters(fts: InternalFilter[]): FilterValue[] {
|
||||
return fts
|
||||
.filter(f => typeof f.value !== 'undefined')
|
||||
.map(({ value, operator, id }) => ({ value, operator, id }));
|
||||
}
|
||||
|
||||
export function extractInputValue(inputType: Filter['input'], event: any) {
|
||||
if (!inputType || inputType === 'text') {
|
||||
return event.currentTarget.value;
|
||||
}
|
||||
|
@ -76,6 +87,13 @@ export function extractInputValue(
|
|||
return null;
|
||||
}
|
||||
|
||||
export function getDefaultFilterOperator(filter: Filter): string {
|
||||
if (filter?.operator) return filter.operator;
|
||||
if (filter?.operators?.length) {
|
||||
return filter.operators[0].value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
interface UseListViewConfig {
|
||||
fetchData: (conf: FetchDataConfig) => any;
|
||||
columns: any[];
|
||||
|
@ -84,6 +102,7 @@ interface UseListViewConfig {
|
|||
initialPageSize: number;
|
||||
initialSort?: SortColumn[];
|
||||
bulkSelectMode?: boolean;
|
||||
initialFilters?: Filter[];
|
||||
bulkSelectColumnConfig?: {
|
||||
id: string;
|
||||
Header: (conf: any) => React.ReactNode;
|
||||
|
@ -97,6 +116,7 @@ export function useListViewState({
|
|||
data,
|
||||
count,
|
||||
initialPageSize,
|
||||
initialFilters = [],
|
||||
initialSort = [],
|
||||
bulkSelectMode = false,
|
||||
bulkSelectColumnConfig,
|
||||
|
@ -123,10 +143,13 @@ export function useListViewState({
|
|||
sortBy: initialSortBy,
|
||||
};
|
||||
|
||||
const columnsWithSelect = useMemo(
|
||||
() => (bulkSelectMode ? [bulkSelectColumnConfig, ...columns] : columns),
|
||||
[bulkSelectMode, columns],
|
||||
);
|
||||
const columnsWithSelect = useMemo(() => {
|
||||
// add exact filter type so filters with falsey values are not filtered out
|
||||
const columnsWithFilter = columns.map(f => ({ ...f, filter: 'exact' }));
|
||||
return bulkSelectMode
|
||||
? [bulkSelectColumnConfig, ...columnsWithFilter]
|
||||
: columnsWithFilter;
|
||||
}, [bulkSelectMode, columns]);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
|
@ -165,6 +188,14 @@ export function useListViewState({
|
|||
query.filters || [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFilters.length) {
|
||||
setInternalFilters(
|
||||
mergeCreateFilterValues(initialFilters, query.filters),
|
||||
);
|
||||
}
|
||||
}, [initialFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams: any = {
|
||||
filters: internalFilters,
|
||||
|
@ -175,22 +206,41 @@ export function useListViewState({
|
|||
queryParams.sortOrder = sortBy[0].desc ? 'desc' : 'asc';
|
||||
}
|
||||
setQuery(queryParams);
|
||||
|
||||
fetchData({ pageIndex, pageSize, sortBy, filters });
|
||||
}, [fetchData, pageIndex, pageSize, sortBy, filters]);
|
||||
|
||||
const filtersApplied = internalFilters.every(
|
||||
({ id, value, operator }, index) =>
|
||||
id &&
|
||||
filters[index] &&
|
||||
filters[index].id === id &&
|
||||
filters[index].value === value &&
|
||||
filters[index]?.id === id &&
|
||||
filters[index]?.value === value &&
|
||||
// @ts-ignore
|
||||
filters[index].operator === operator,
|
||||
filters[index]?.operator === operator,
|
||||
);
|
||||
|
||||
const updateInternalFilter = (index: number, update: object) =>
|
||||
setInternalFilters(updateInList(internalFilters, index, update));
|
||||
|
||||
const applyFilterValue = (index: number, value: any) => {
|
||||
// skip redunundant updates
|
||||
if (internalFilters[index].value === value) {
|
||||
return;
|
||||
}
|
||||
const update = { ...internalFilters[index], value };
|
||||
const updatedFilters = updateInList(internalFilters, index, update);
|
||||
setInternalFilters(updatedFilters);
|
||||
setAllFilters(convertFilters(updatedFilters));
|
||||
};
|
||||
|
||||
const removeFilterAndApply = (index: number) => {
|
||||
const updated = removeFromList(internalFilters, index);
|
||||
setInternalFilters(updated);
|
||||
setAllFilters(convertFilters(updated));
|
||||
};
|
||||
|
||||
return {
|
||||
applyFilters: () => setAllFilters(convertFilters(internalFilters)),
|
||||
removeFilterAndApply,
|
||||
canNextPage,
|
||||
canPreviousPage,
|
||||
filtersApplied,
|
||||
|
@ -205,7 +255,7 @@ export function useListViewState({
|
|||
setAllFilters,
|
||||
setInternalFilters,
|
||||
state: { pageIndex, pageSize, sortBy, filters, internalFilters },
|
||||
updateInternalFilter: (index: number, update: object) =>
|
||||
setInternalFilters(updateInList(internalFilters, index, update)),
|
||||
updateInternalFilter,
|
||||
applyFilterValue,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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 styled from '@emotion/styled';
|
||||
|
||||
export default styled.input`
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
padding: 4px 8px;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
`;
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 styled from '@emotion/styled';
|
||||
// @ts-ignore
|
||||
import Select from 'react-select';
|
||||
|
||||
export default styled(Select)`
|
||||
display: inline;
|
||||
&.is-focused:not(.is-open) > .Select-control {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.Select-control {
|
||||
display: inline-table;
|
||||
border: none;
|
||||
width: 100px;
|
||||
&:focus,
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
.Select-menu-outer {
|
||||
margin-top: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
`;
|
|
@ -26,6 +26,7 @@ export enum FeatureFlag {
|
|||
ESTIMATE_QUERY_COST = 'ESTIMATE_QUERY_COST',
|
||||
SHARE_QUERIES_VIA_KV_STORE = 'SHARE_QUERIES_VIA_KV_STORE',
|
||||
SQLLAB_BACKEND_PERSISTENCE = 'SQLLAB_BACKEND_PERSISTENCE',
|
||||
LIST_VIEWS_NEW_UI = 'LIST_VIEWS_NEW_UI',
|
||||
}
|
||||
|
||||
export type FeatureFlagMap = {
|
||||
|
|
|
@ -68,7 +68,6 @@ import {
|
|||
declare module 'react-table' {
|
||||
export interface TableOptions<D extends object>
|
||||
extends UseExpandedOptions<D>,
|
||||
UseFiltersOptions<D>,
|
||||
UseFiltersOptions<D>,
|
||||
UseGlobalFiltersOptions<D>,
|
||||
UseGroupByOptions<D>,
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from 'src/components/ListView/types';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
|
@ -125,6 +126,10 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
return this.hasPerm('can_mulexport');
|
||||
}
|
||||
|
||||
get isNewUIEnabled() {
|
||||
return isFeatureEnabled(FeatureFlag.LIST_VIEWS_NEW_UI);
|
||||
}
|
||||
|
||||
initialSort = [{ id: 'changed_on', desc: true }];
|
||||
|
||||
columns = [
|
||||
|
@ -378,6 +383,39 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
|
||||
updateFilters = () => {
|
||||
const { filterOperators, owners } = this.state;
|
||||
|
||||
if (this.isNewUIEnabled) {
|
||||
return this.setState({
|
||||
filters: [
|
||||
{
|
||||
Header: 'Owner',
|
||||
id: 'owners',
|
||||
input: 'select',
|
||||
operator: 'rel_m_m',
|
||||
unfilteredLabel: 'All',
|
||||
selects: owners.map(({ text: label, value }) => ({ label, value })),
|
||||
},
|
||||
{
|
||||
Header: 'Published',
|
||||
id: 'published',
|
||||
input: 'select',
|
||||
operator: 'eq',
|
||||
unfilteredLabel: 'Any',
|
||||
selects: [
|
||||
{ label: 'Published', value: true },
|
||||
{ label: 'Unpublished', value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: 'Search',
|
||||
id: 'dashboard_title',
|
||||
input: 'search',
|
||||
operator: 'title_or_slug',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const convertFilter = ({
|
||||
name: label,
|
||||
operator,
|
||||
|
@ -386,7 +424,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
operator: string;
|
||||
}) => ({ label, value: operator });
|
||||
|
||||
this.setState({
|
||||
return this.setState({
|
||||
filters: [
|
||||
{
|
||||
Header: 'Dashboard',
|
||||
|
@ -481,6 +519,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
|||
initialSort={this.initialSort}
|
||||
filters={filters}
|
||||
bulkActions={bulkActions}
|
||||
useNewUIFilters={this.isNewUIEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,10 @@ import thunk from 'redux-thunk';
|
|||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'emotion-theming';
|
||||
|
||||
import { initFeatureFlags } from 'src/featureFlags';
|
||||
import { supersetTheme } from 'stylesheets/styled-components/superset-theme';
|
||||
import Menu from 'src/components/Menu/Menu';
|
||||
import DashboardList from 'src/views/dashboardList/DashboardList';
|
||||
import ChartList from 'src/views/chartList/ChartList';
|
||||
|
@ -41,6 +44,8 @@ const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
|
|||
const user = { ...bootstrap.user };
|
||||
const menu = { ...bootstrap.common.menu_data };
|
||||
|
||||
initFeatureFlags(bootstrap.common.feature_flags);
|
||||
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
messageToasts: messageToastReducer,
|
||||
|
@ -51,24 +56,26 @@ const store = createStore(
|
|||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Menu data={menu} />
|
||||
<Switch>
|
||||
<Route path="/superset/welcome/">
|
||||
<Welcome user={user} />
|
||||
</Route>
|
||||
<Route path="/dashboard/list/">
|
||||
<DashboardList user={user} />
|
||||
</Route>
|
||||
<Route path="/chart/list/">
|
||||
<ChartList user={user} />
|
||||
</Route>
|
||||
<Route path="/tablemodelview/list/">
|
||||
<DatasetList user={user} />
|
||||
</Route>
|
||||
</Switch>
|
||||
<ToastPresenter />
|
||||
</Router>
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Router>
|
||||
<Menu data={menu} />
|
||||
<Switch>
|
||||
<Route path="/superset/welcome/">
|
||||
<Welcome user={user} />
|
||||
</Route>
|
||||
<Route path="/dashboard/list/">
|
||||
<DashboardList user={user} />
|
||||
</Route>
|
||||
<Route path="/chart/list/">
|
||||
<ChartList user={user} />
|
||||
</Route>
|
||||
<Route path="/tablemodelview/list/">
|
||||
<DatasetList user={user} />
|
||||
</Route>
|
||||
</Switch>
|
||||
<ToastPresenter />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* 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 styled, { CreateStyled } from '@emotion/styled';
|
||||
|
||||
const defaultTheme = {
|
||||
borderRadius: '4px',
|
||||
colors: {
|
||||
primary: {
|
||||
base: '#20A7C9',
|
||||
},
|
||||
secondary: {
|
||||
base: '#444E7C',
|
||||
dark1: '#363E63',
|
||||
dark2: '#282E4A',
|
||||
dark3: '#1B1F31',
|
||||
light1: '#8E94B0',
|
||||
light2: '#B4B8CA',
|
||||
light3: '#D9DBE4',
|
||||
light4: '#ECEEF2',
|
||||
light5: '#F5F5F8',
|
||||
},
|
||||
},
|
||||
gridUnit: '4px',
|
||||
};
|
||||
|
||||
export default styled as CreateStyled<typeof defaultTheme>;
|
||||
|
||||
export const supersetTheme = defaultTheme;
|
|
@ -154,6 +154,7 @@ const babelLoader = {
|
|||
// disable gzip compression for cache files
|
||||
// faster when there are millions of small files
|
||||
cacheCompression: false,
|
||||
plugins: ['emotion'],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -198,6 +199,7 @@ const config = {
|
|||
alias: {
|
||||
src: path.resolve(APP_DIR, './src'),
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
stylesheets: path.resolve(APP_DIR, './stylesheets'),
|
||||
},
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
symlinks: false,
|
||||
|
|
|
@ -289,6 +289,7 @@ DEFAULT_FEATURE_FLAGS = {
|
|||
"SHARE_QUERIES_VIA_KV_STORE": False,
|
||||
"TAGGING_SYSTEM": False,
|
||||
"SQLLAB_BACKEND_PERSISTENCE": False,
|
||||
"LIST_VIEWS_NEW_UI": False,
|
||||
}
|
||||
|
||||
# This is merely a default.
|
||||
|
|
Loading…
Reference in New Issue