This is an automated email from the ASF dual-hosted git repository.
tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 92f2353 feat: filters for database list view (#10772)
92f2353 is described below
commit 92f2353f80568c419d1cd183e54b2115479b80a8
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Fri Sep 4 16:23:38 2020 -0700
feat: filters for database list view (#10772)
---
.../components/ListView/ListView_spec.jsx | 11 ++++++
.../views/CRUD/data/database/DatabaseList_spec.jsx | 29 +++++++++++++++
.../src/components/ListView/Filters.tsx | 18 +++++++---
.../src/components/ListView/ListView.tsx | 12 ++++++-
superset-frontend/src/components/ListView/types.ts | 4 ++-
superset-frontend/src/components/ListView/utils.ts | 24 ++++++++-----
superset-frontend/src/components/SearchInput.tsx | 7 ++--
.../src/views/CRUD/data/database/DatabaseList.tsx | 42 +++++++++++++++++++++-
8 files changed, 128 insertions(+), 19 deletions(-)
diff --git
a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index a60f5ed..120b349 100644
--- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
@@ -23,6 +23,7 @@ import { QueryParamProvider } from 'use-query-params';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import Button from 'src/components/Button';
+import { Empty } from 'src/common/components';
import CardCollection from 'src/components/ListView/CardCollection';
import { CardSortSelect } from 'src/components/ListView/CardSortSelect';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
@@ -346,6 +347,16 @@ describe('ListView', () => {
);
});
+ it('renders and empty state when there is no data', () => {
+ const props = {
+ ...mockedProps,
+ data: [],
+ };
+
+ const wrapper2 = factory(props);
+ expect(wrapper2.find(Empty)).toExist();
+ });
+
it('renders UI filters', () => {
expect(wrapper.find(ListViewFilters)).toExist();
});
diff --git
a/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
b/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
index 25f0fbd..9c3ea6a 100644
---
a/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
+++
b/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
@@ -26,6 +26,7 @@ import DatabaseList from
'src/views/CRUD/data/database/DatabaseList';
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
import SubMenu from 'src/components/Menu/SubMenu';
import ListView from 'src/components/ListView';
+import Filters from 'src/components/ListView/Filters';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { act } from 'react-dom/test-utils';
@@ -116,4 +117,32 @@ describe('DatabaseList', () => {
expect(fetchMock.calls(/database\/0/, 'DELETE')).toHaveLength(1);
});
+
+ it('filters', async () => {
+ const filtersWrapper = wrapper.find(Filters);
+ act(() => {
+ filtersWrapper
+ .find('[name="expose_in_sqllab"]')
+ .first()
+ .props()
+ .onSelect(true);
+
+ filtersWrapper
+ .find('[name="allow_run_async"]')
+ .first()
+ .props()
+ .onSelect(false);
+
+ filtersWrapper
+ .find('[name="database_name"]')
+ .first()
+ .props()
+ .onSubmit('fooo');
+ });
+ await waitForComponentToPaint(wrapper);
+
+ expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
+
`"http://localhost/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t),(col:allow_run_async,opr:eq,value:!f),(col:database_name,opr:ct,value:fooo)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
+ );
+ });
});
diff --git a/superset-frontend/src/components/ListView/Filters.tsx
b/superset-frontend/src/components/ListView/Filters.tsx
index a205031..370e3dc 100644
--- a/superset-frontend/src/components/ListView/Filters.tsx
+++ b/superset-frontend/src/components/ListView/Filters.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useState } from 'react';
+import React, { useState, ReactNode } from 'react';
import { styled, withTheme, SupersetThemeProps } from '@superset-ui/style';
import {
@@ -36,7 +36,7 @@ import {
import { filterSelectStyles } from './utils';
interface BaseFilter {
- Header: string;
+ Header: ReactNode;
initialValue: any;
}
interface SelectFilterProps extends BaseFilter {
@@ -130,7 +130,7 @@ function SelectFilter({
return (
<FilterContainer>
- <FilterTitle>{Header}</FilterTitle>
+ <FilterTitle>{Header}:</FilterTitle>
{fetchSelects ? (
<PaginatedSelect
data-test="filters-select"
@@ -168,9 +168,15 @@ const StyledSelectFilter = withTheme(SelectFilter);
interface SearchHeaderProps extends BaseFilter {
Header: string;
onSubmit: (val: string) => void;
+ name: string;
}
-function SearchFilter({ Header, initialValue, onSubmit }: SearchHeaderProps) {
+function SearchFilter({
+ Header,
+ name,
+ initialValue,
+ onSubmit,
+}: SearchHeaderProps) {
const [value, setValue] = useState(initialValue || '');
const handleSubmit = () => onSubmit(value);
const onClear = () => {
@@ -183,6 +189,7 @@ function SearchFilter({ Header, initialValue, onSubmit }:
SearchHeaderProps) {
<SearchInput
data-test="filters-search"
placeholder={Header}
+ name={name}
value={value}
onChange={e => {
setValue(e.currentTarget.value);
@@ -244,12 +251,13 @@ function UIFilters({
/>
);
}
- if (input === 'search') {
+ if (input === 'search' && typeof Header === 'string') {
return (
<SearchFilter
Header={Header}
initialValue={initialValue}
key={id}
+ name={id}
onSubmit={(value: string) => updateFilterValue(index, value)}
/>
);
diff --git a/superset-frontend/src/components/ListView/ListView.tsx
b/superset-frontend/src/components/ListView/ListView.tsx
index 54fdeae..cafec4b 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { t } from '@superset-ui/translation';
import React, { useState } from 'react';
+import { t } from '@superset-ui/translation';
import { Alert } from 'react-bootstrap';
+import { Empty } from 'src/common/components';
import styled from '@superset-ui/style';
import cx from 'classnames';
import Button from 'src/components/Button';
@@ -140,6 +141,10 @@ const ViewModeContainer = styled.div`
}
`;
+const EmptyWrapper = styled.div`
+ margin: ${({ theme }) => theme.gridUnit * 40}px 0;
+`;
+
const ViewModeToggle = ({
mode,
setMode,
@@ -348,6 +353,11 @@ function ListView<T extends object = any>({
loading={loading}
/>
)}
+ {!loading && rows.length === 0 && (
+ <EmptyWrapper>
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
+ </EmptyWrapper>
+ )}
</div>
</div>
diff --git a/superset-frontend/src/components/ListView/types.ts
b/superset-frontend/src/components/ListView/types.ts
index 1161a88..2efb5f0 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { ReactNode } from 'react';
+
export interface SortColumn {
id: string;
desc?: boolean;
@@ -36,7 +38,7 @@ export interface CardSortSelectOption {
}
export interface Filter {
- Header: string;
+ Header: ReactNode;
id: string;
operators?: SelectOption[];
operator?:
diff --git a/superset-frontend/src/components/ListView/utils.ts
b/superset-frontend/src/components/ListView/utils.ts
index 026d9e7..c8ecc95 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -227,15 +227,21 @@ export function useListViewState({
}, [query]);
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));
- gotoPage(0); // clear pagination on filter
+ setInternalFilters(currentInternalFilters => {
+ // skip redunundant updates
+ if (currentInternalFilters[index].value === value) {
+ return currentInternalFilters;
+ }
+ const update = { ...currentInternalFilters[index], value };
+ const updatedFilters = updateInList(
+ currentInternalFilters,
+ index,
+ update,
+ );
+ setAllFilters(convertFilters(updatedFilters));
+ gotoPage(0); // clear pagination on filter
+ return updatedFilters;
+ });
};
return {
diff --git a/superset-frontend/src/components/SearchInput.tsx
b/superset-frontend/src/components/SearchInput.tsx
index 314c9a4..2d00775 100644
--- a/superset-frontend/src/components/SearchInput.tsx
+++ b/superset-frontend/src/components/SearchInput.tsx
@@ -20,12 +20,13 @@ import styled from '@superset-ui/style';
import React from 'react';
import Icon from 'src/components/Icon';
-interface Props {
+interface SearchInputProps {
onSubmit: () => void;
onClear: () => void;
value: string;
onChange: React.EventHandler<React.ChangeEvent<HTMLInputElement>>;
placeholder?: string;
+ name?: string;
}
const SearchInputWrapper = styled.div`
@@ -68,8 +69,9 @@ export default function SearchInput({
onClear,
onSubmit,
placeholder = 'Search',
+ name,
value,
-}: Props) {
+}: SearchInputProps) {
return (
<SearchInputWrapper>
<SearchIcon
@@ -89,6 +91,7 @@ export default function SearchInput({
placeholder={placeholder}
onChange={onChange}
value={value}
+ name={name}
/>
{value && (
<ClearIcon
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
index 646ed37..a852619 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
@@ -245,7 +245,47 @@ function DatabaseList({ addDangerToast, addSuccessToast }:
DatabaseListProps) {
[canDelete, canCreate],
);
- const filters: Filters = [];
+ const filters: Filters = useMemo(
+ () => [
+ {
+ Header: t('Expose in SQL Lab'),
+ id: 'expose_in_sqllab',
+ input: 'select',
+ operator: 'eq',
+ unfilteredLabel: 'All',
+ selects: [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false },
+ ],
+ },
+ {
+ Header: (
+ <TooltipWrapper
+ label="allow-run-async-filter-header"
+ tooltip={t('Asynchronous Query Execution')}
+ placement="top"
+ >
+ <span>{t('AQE')}</span>
+ </TooltipWrapper>
+ ),
+ id: 'allow_run_async',
+ input: 'select',
+ operator: 'eq',
+ unfilteredLabel: 'All',
+ selects: [
+ { label: 'Yes', value: true },
+ { label: 'No', value: false },
+ ],
+ },
+ {
+ Header: t('Search'),
+ id: 'database_name',
+ input: 'search',
+ operator: 'ct',
+ },
+ ],
+ [],
+ );
return (
<>