diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index a60f5ed4e68f3..120b34944b182 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 25f0fbd4cc371..9c3ea6a632d86 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 a20503194d7e6..370e3dcd88840 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 ( - {Header} + {Header}: {fetchSelects ? ( 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) { { setValue(e.currentTarget.value); @@ -244,12 +251,13 @@ function UIFilters({ /> ); } - if (input === 'search') { + if (input === 'search' && typeof Header === 'string') { return ( updateFilterValue(index, value)} /> ); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 54fdeaec8e719..cafec4b292b42 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({ loading={loading} /> )} + {!loading && rows.length === 0 && ( + + + + )} diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 1161a888f0521..2efb5f0a183a6 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 026d9e7a13ff3..c8ecc95be5c39 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 314c9a41019f4..2d0077555d836 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>; 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 ( {value && ( [ + { + 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: ( + + {t('AQE')} + + ), + 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 ( <>