refactor: Changes the list views to use the new Select component (#16393)

* chore: Change the list views to use the new Select component

* Fix Cypress tests

* Enables search for all controls

* Adjusts controls width

* Removes 'Me' and keeps the logged user on top

* Fixes tests

* Uses the borderless version for the filters

* Fixes the tests

* Reverts the Select theme to the default

* Rebases and fixes js error

* Fixes failing test

* Removes unused withTheme
This commit is contained in:
Michael S. Molina 2021-09-22 07:44:18 -03:00 committed by GitHub
parent 596e1cdf9b
commit b6d78bf4f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 510 additions and 835 deletions

View File

@ -27,44 +27,34 @@ describe('chart card view filters', () => {
it('should filter by owners correctly', () => {
// filter by owners
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="styled-card"]').should('not.exist');
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="styled-card"]').should('not.exist');
});
it('should filter by me correctly', () => {
// filter by me
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
});
it('should filter by created by correctly', () => {
// filter by created by
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('.ant-card').should('not.exist');
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="styled-card"]').should('not.exist');
});
xit('should filter by viz type correctly', () => {
// filter by viz type
cy.get('.Select__control').eq(2).click();
cy.get('.Select__menu').contains('area').click({ timeout: 5000 });
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('.rc-virtual-list').contains('area').click({ timeout: 5000 });
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
cy.get('[data-test="styled-card"]')
.contains("World's Pop Growth")
.should('be.visible');
cy.get('.Select__control').eq(2).click();
cy.get('.Select__control').eq(2).type('world_map{enter}');
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('[data-test="filters-select"]').eq(2).type('world_map{enter}');
cy.get('[data-test="styled-card"]').should('have.length', 1);
cy.get('[data-test="styled-card"]')
.contains('% Rural')
@ -73,14 +63,16 @@ describe('chart card view filters', () => {
it('should filter by datasource correctly', () => {
// filter by datasource
cy.get('.Select__control').eq(3).click();
cy.get('.Select__menu').contains('unicode_test').click();
cy.get('[data-test="filters-select"]').eq(3).click();
cy.get('.rc-virtual-list').contains('unicode_test').click();
cy.get('[data-test="styled-card"]').should('have.length', 1);
cy.get('[data-test="styled-card"]')
.contains('Unicode Cloud')
.should('be.visible');
cy.get('.Select__control').eq(2).click();
cy.get('.Select__control').eq(2).type('energy_usage{enter}{enter}');
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('[data-test="filters-select"]')
.eq(2)
.type('energy_usage{enter}{enter}');
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
});
});
@ -94,57 +86,49 @@ describe('chart list view filters', () => {
it('should filter by owners correctly', () => {
// filter by owners
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="table-row"]').should('not.exist');
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="table-row"]').should('not.exist');
});
it('should filter by me correctly', () => {
// filter by me
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
});
it('should filter by created by correctly', () => {
// filter by created by
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="table-row"]').should('not.exist');
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="table-row"]').should('not.exist');
});
// this is flaky, but seems to fail along with the card view test of the same name
xit('should filter by viz type correctly', () => {
// filter by viz type
cy.get('.Select__control').eq(2).click();
cy.get('.Select__menu').contains('area').click({ timeout: 5000 });
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('.rc-virtual-list').contains('area').click({ timeout: 5000 });
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
cy.get('[data-test="table-row"]')
.contains("World's Pop Growth")
.should('exist');
cy.get('.Select__control').eq(2).click();
cy.get('.Select__control').eq(2).type('world_map{enter}');
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('[data-test="filters-select"]').eq(2).type('world_map{enter}');
cy.get('[data-test="table-row"]').should('have.length', 1);
cy.get('[data-test="table-row"]').contains('% Rural').should('exist');
});
it('should filter by datasource correctly', () => {
// filter by datasource
cy.get('.Select__control').eq(3).click();
cy.get('.Select__menu').contains('unicode_test').click();
cy.get('[data-test="filters-select"]').eq(3).click();
cy.get('.rc-virtual-list').contains('unicode_test').click();
cy.get('[data-test="table-row"]').should('have.length', 1);
cy.get('[data-test="table-row"]').contains('Unicode Cloud').should('exist');
cy.get('.Select__control').eq(3).click();
cy.get('.Select__control').eq(3).type('energy_usage{enter}{enter}');
cy.get('[data-test="filters-select"]').eq(3).click();
cy.get('[data-test="filters-select"]')
.eq(3)
.type('energy_usage{enter}{enter}');
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
});
});

View File

@ -27,44 +27,34 @@ describe('dashboard filters card view', () => {
it('should filter by owners correctly', () => {
// filter by owners
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="styled-card"]').should('not.exist');
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="styled-card"]').should('not.exist');
});
it('should filter by me correctly', () => {
// filter by me
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="styled-card"]').its('length').should('be.gt', 0);
});
it('should filter by created by correctly', () => {
// filter by created by
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('.ant-card').should('not.exist');
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('.ant-card').should('not.exist');
});
it('should filter by published correctly', () => {
// filter by published
cy.get('.Select__control').eq(2).click();
cy.get('.Select__menu').contains('Published').click({ timeout: 5000 });
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('.rc-virtual-list').contains('Published').click({ timeout: 5000 });
cy.get('[data-test="styled-card"]').should('have.length', 3);
cy.get('[data-test="styled-card"]')
.contains('USA Births Names')
.should('be.visible');
cy.get('.Select__control').eq(1).click();
cy.get('.Select__control').eq(1).type('unpub{enter}');
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('[data-test="filters-select"]').eq(1).type('unpub{enter}');
cy.get('[data-test="styled-card"]').should('have.length', 3);
});
});
@ -78,44 +68,34 @@ describe('dashboard filters list view', () => {
it('should filter by owners correctly', () => {
// filter by owners
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="table-row"]').should('not.exist');
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').first().click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="table-row"]').should('not.exist');
});
it('should filter by me correctly', () => {
// filter by me
cy.get('.Select__control').first().click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('me').click();
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
});
it('should filter by created by correctly', () => {
// filter by created by
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('alpha user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('alpha user').click();
cy.get('[data-test="table-row"]').should('not.exist');
cy.get('.Select__control').eq(1).click();
cy.get('.Select__menu').contains('gamma user').click();
cy.get('[data-test="filters-select"]').eq(1).click();
cy.get('.rc-virtual-list').contains('gamma user').click();
cy.get('[data-test="table-row"]').should('not.exist');
});
it('should filter by published correctly', () => {
// filter by published
cy.get('.Select__control').eq(2).click();
cy.get('.Select__menu').contains('Published').click();
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('.rc-virtual-list').contains('Published').click();
cy.get('[data-test="table-row"]').should('have.length', 3);
cy.get('[data-test="table-row"]')
.contains('USA Births Names')
.should('be.visible');
cy.get('.Select__control').eq(2).click();
cy.get('.Select__control').eq(2).type('unpub{enter}');
cy.get('[data-test="filters-select"]').eq(2).click();
cy.get('[data-test="filters-select"]').eq(2).type('unpub{enter}');
cy.get('[data-test="table-row"]').should('have.length', 3);
});
});

View File

@ -17,6 +17,12 @@
* under the License.
*/
import { DatePicker as AntdDatePicker } from 'antd';
import { styled } from '@superset-ui/core';
const AntdRangePicker = AntdDatePicker.RangePicker;
export const RangePicker = styled(AntdRangePicker)`
border-radius: ${({ theme }) => theme.gridUnit}px;
`;
export const { RangePicker } = AntdDatePicker;
export const DatePicker = AntdDatePicker;

View File

@ -16,24 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { styled, withTheme, SupersetThemeProps, t } from '@superset-ui/core';
import { PartialThemeConfig, Select } from 'src/components/Select';
import React, { useState, useMemo } from 'react';
import { styled, t } from '@superset-ui/core';
import { Select } from 'src/components';
import { FormLabel } from 'src/components/Form';
import { SELECT_WIDTH } from './utils';
import { CardSortSelectOption, FetchDataConfig, SortColumn } from './types';
import { filterSelectStyles } from './utils';
const SortTitle = styled.label`
font-weight: bold;
line-height: 27px;
margin: 0 0.4em 0 0;
`;
const SortContainer = styled.div`
display: inline-flex;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
padding-top: ${({ theme }) => theme.gridUnit}px;
align-items: center;
text-align: left;
width: ${SELECT_WIDTH}px;
`;
interface CardViewSelectSortProps {
onChange: (conf: FetchDataConfig) => any;
options: Array<CardSortSelectOption>;
@ -42,43 +39,6 @@ interface CardViewSelectSortProps {
pageSize: number;
}
interface StyledSelectProps {
onChange: (value: CardSortSelectOption) => void;
options: CardSortSelectOption[];
selectStyles: any;
theme: SupersetThemeProps['theme'];
value: CardSortSelectOption;
}
function StyledSelect({
onChange,
options,
selectStyles,
theme,
value,
}: StyledSelectProps) {
const filterSelectTheme: PartialThemeConfig = {
spacing: {
baseUnit: 1,
fontSize: theme.typography.sizes.s,
minWidth: '5em',
},
};
return (
<Select
data-test="card-sort-select"
clearable={false}
onChange={onChange}
options={options}
stylesConfig={selectStyles}
themeConfig={filterSelectTheme}
value={value}
/>
);
}
const StyledCardSortSelect = withTheme(StyledSelect);
export const CardSortSelect = ({
initialSort,
onChange,
@ -87,25 +47,45 @@ export const CardSortSelect = ({
pageSize,
}: CardViewSelectSortProps) => {
const defaultSort =
initialSort && options.find(({ id }) => id === initialSort[0].id);
const [selectedOption, setSelectedOption] = useState<CardSortSelectOption>(
defaultSort || options[0],
(initialSort && options.find(({ id }) => id === initialSort[0].id)) ||
options[0];
const [value, setValue] = useState({
label: defaultSort.label,
value: defaultSort.value,
});
const formattedOptions = useMemo(
() => options.map(option => ({ label: option.label, value: option.value })),
[options],
);
const handleOnChange = (selected: CardSortSelectOption) => {
setSelectedOption(selected);
const sortBy = [{ id: selected.id, desc: selected.desc }];
onChange({ pageIndex, pageSize, sortBy, filters: [] });
const handleOnChange = (selected: { label: string; value: string }) => {
setValue(selected);
const originalOption = options.find(
({ value }) => value === selected.value,
);
if (originalOption) {
const sortBy = [
{
id: originalOption.id,
desc: originalOption.desc,
},
];
onChange({ pageIndex, pageSize, sortBy, filters: [] });
}
};
return (
<SortContainer>
<SortTitle>{t('Sort:')}</SortTitle>
<StyledCardSortSelect
<Select
ariaLabel={t('Sort')}
header={<FormLabel>{t('Sort')}</FormLabel>}
labelInValue
onChange={(value: CardSortSelectOption) => handleOnChange(value)}
options={options}
selectStyles={filterSelectStyles}
value={selectedOption}
options={formattedOptions}
showSearch
value={value}
/>
</SortContainer>
);

View File

@ -18,6 +18,7 @@
*/
import { ReactNode } from 'react';
import { styled } from '@superset-ui/core';
import { SELECT_WIDTH } from 'src/components/ListView/utils';
export interface BaseFilter {
Header: ReactNode;
@ -26,12 +27,7 @@ export interface BaseFilter {
export const FilterContainer = styled.div`
display: inline-flex;
margin-right: 2em;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
align-items: center;
`;
export const FilterTitle = styled.label`
font-weight: bold;
margin: 0 0.4em 0 0;
width: ${SELECT_WIDTH}px;
`;

View File

@ -19,8 +19,9 @@
import React, { useState, useMemo } from 'react';
import moment, { Moment } from 'moment';
import { styled } from '@superset-ui/core';
import { RangePicker as AntRangePicker } from 'src/components/DatePicker';
import { FilterContainer, BaseFilter, FilterTitle } from './Base';
import { RangePicker } from 'src/components/DatePicker';
import { FormLabel } from 'src/components/Form';
import { BaseFilter } from './Base';
interface DateRangeFilterProps extends BaseFilter {
onSubmit: (val: number[]) => void;
@ -29,13 +30,12 @@ interface DateRangeFilterProps extends BaseFilter {
type ValueState = [number, number];
const RangePicker = styled(AntRangePicker)`
padding: 0 11px;
transform: translateX(-7px);
`;
const RangeFilterContainer = styled(FilterContainer)`
margin-right: 1em;
const RangeFilterContainer = styled.div`
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
width: 360px;
`;
export default function DateRangeFilter({
@ -51,10 +51,9 @@ export default function DateRangeFilter({
return (
<RangeFilterContainer>
<FilterTitle>{Header}:</FilterTitle>
<FormLabel>{Header}</FormLabel>
<RangePicker
showTime
bordered={false}
value={momentValue}
onChange={momentRange => {
if (!momentRange) {

View File

@ -17,8 +17,12 @@
* under the License.
*/
import React, { useState } from 'react';
import SearchInput from 'src/components/SearchInput';
import { FilterContainer, BaseFilter } from './Base';
import { t, styled } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { AntdInput as Input } from 'src/common/components';
import { SELECT_WIDTH } from 'src/components/ListView/utils';
import { FormLabel } from 'src/components/Form';
import { BaseFilter } from './Base';
interface SearchHeaderProps extends BaseFilter {
Header: string;
@ -26,6 +30,18 @@ interface SearchHeaderProps extends BaseFilter {
name: string;
}
const Container = styled.div`
width: ${SELECT_WIDTH}px;
`;
const SearchIcon = styled(Icons.Search)`
color: ${({ theme }) => theme.colors.grayscale.light1};
`;
const StyledInput = styled(Input)`
border-radius: ${({ theme }) => theme.gridUnit}px;
`;
export default function SearchFilter({
Header,
name,
@ -38,28 +54,27 @@ export default function SearchFilter({
onSubmit(value.trim());
}
};
const onClear = () => {
setValue('');
onSubmit('');
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.currentTarget.value);
if (e.currentTarget.value === '') {
onClear();
onSubmit('');
}
};
return (
<FilterContainer>
<SearchInput
<Container>
<FormLabel>{Header}</FormLabel>
<StyledInput
allowClear
data-test="filters-search"
placeholder={Header}
placeholder={t('Type a value')}
name={name}
value={value}
onChange={handleChange}
onSubmit={handleSubmit}
onClear={onClear}
onPressEnter={handleSubmit}
onBlur={handleSubmit}
prefix={<SearchIcon iconSize="l" />}
/>
</FilterContainer>
</Container>
);
}

View File

@ -16,138 +16,76 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { withTheme, SupersetThemeProps } from '@superset-ui/core';
import {
Select,
PaginatedSelect,
PartialThemeConfig,
} from 'src/components/Select';
import React, { useState, useMemo } from 'react';
import { t } from '@superset-ui/core';
import { Select } from 'src/components';
import { Filter, SelectOption } from 'src/components/ListView/types';
import { filterSelectStyles } from 'src/components/ListView/utils';
import { FilterContainer, BaseFilter, FilterTitle } from './Base';
import { FormLabel } from 'src/components/Form';
import { FilterContainer, BaseFilter } from './Base';
interface SelectFilterProps extends BaseFilter {
emptyLabel?: string;
fetchSelects?: Filter['fetchSelects'];
name?: string;
onSelect: (selected: any) => any;
onSelect: (selected: SelectOption | undefined) => void;
paginate?: boolean;
selects: Filter['selects'];
theme: SupersetThemeProps['theme'];
}
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
function SelectFilter({
Header,
emptyLabel = 'None',
name,
fetchSelects,
initialValue,
onSelect,
paginate = false,
selects = [],
theme,
}: SelectFilterProps) {
const filterSelectTheme: PartialThemeConfig = {
spacing: {
baseUnit: 2,
fontSize: theme.typography.sizes.s,
minWidth: '5em',
},
};
const [selectedOption, setSelectedOption] = useState(initialValue);
const clearFilterSelect = {
label: emptyLabel,
value: CLEAR_SELECT_FILTER_VALUE,
};
const options = [clearFilterSelect, ...selects];
let initialOption = clearFilterSelect;
// Set initial value if not async
if (!fetchSelects) {
const matchingOption = options.find(x => x.value === initialValue);
if (matchingOption) {
initialOption = matchingOption;
}
}
const [selectedOption, setSelectedOption] = useState(initialOption);
const onChange = (selected: SelectOption | null) => {
if (selected === null) return;
const onChange = (selected: SelectOption) => {
onSelect(
selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value,
selected ? { label: selected.label, value: selected.value } : undefined,
);
setSelectedOption(selected);
};
const fetchAndFormatSelects = async (
inputValue: string,
loadedOptions: SelectOption[],
{ page }: { page: number },
) => {
// only include clear filter when filter value does not exist
let result = inputValue || page > 0 ? [] : [clearFilterSelect];
let hasMore = paginate;
if (fetchSelects) {
const selectValues = await fetchSelects(inputValue, page);
// update matching option at initial load
if (!selectValues.length) {
hasMore = false;
}
result = [...result, ...selectValues];
const matchingOption = result.find(x => x.value === initialValue);
if (matchingOption) {
setSelectedOption(matchingOption);
}
}
return {
options: result,
hasMore,
additional: {
page: page + 1,
},
};
const onClear = () => {
onSelect(undefined);
setSelectedOption(undefined);
};
const fetchAndFormatSelects = useMemo(
() => async (inputValue: string, page: number, pageSize: number) => {
if (fetchSelects) {
const selectValues = await fetchSelects(inputValue, page, pageSize);
return {
data: selectValues.data,
totalCount: selectValues.totalCount,
};
}
return {
data: [],
totalCount: 0,
};
},
[fetchSelects],
);
return (
<FilterContainer>
<FilterTitle>{Header}:</FilterTitle>
{fetchSelects ? (
<PaginatedSelect
data-test="filters-select"
defaultOptions
themeConfig={filterSelectTheme}
stylesConfig={filterSelectStyles}
// @ts-ignore
value={selectedOption}
// @ts-ignore
onChange={onChange}
// @ts-ignore
loadOptions={fetchAndFormatSelects}
placeholder={emptyLabel}
clearable={false}
additional={{
page: 0,
}}
/>
) : (
<Select
data-test="filters-select"
themeConfig={filterSelectTheme}
stylesConfig={filterSelectStyles}
value={selectedOption}
options={options}
onChange={onChange}
clearable={false}
/>
)}
<Select
allowClear
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
labelInValue
data-test="filters-select"
header={<FormLabel>{Header}</FormLabel>}
onChange={onChange}
onClear={onClear}
options={fetchSelects ? fetchAndFormatSelects : selects}
placeholder={t('Select or type a value')}
showSearch
value={selectedOption}
/>
</FilterContainer>
);
}
export default withTheme(SelectFilter);
export default SelectFilter;

View File

@ -17,12 +17,13 @@
* under the License.
*/
import React from 'react';
import { styled, withTheme } from '@superset-ui/core';
import { withTheme } from '@superset-ui/core';
import {
FilterValue,
Filters,
InternalFilter,
SelectOption,
} from 'src/components/ListView/types';
import SearchFilter from './Search';
import SelectFilter from './Select';
@ -34,42 +35,28 @@ interface UIFiltersProps {
updateFilterValue: (id: number, value: FilterValue['value']) => void;
}
const FilterWrapper = styled.div`
display: inline-block;
`;
function UIFilters({
filters,
internalFilters = [],
updateFilterValue,
}: UIFiltersProps) {
return (
<FilterWrapper>
<>
{filters.map(
(
{
Header,
fetchSelects,
id,
input,
paginate,
selects,
unfilteredLabel,
},
index,
) => {
({ Header, fetchSelects, id, input, paginate, selects }, index) => {
const initialValue =
internalFilters[index] && internalFilters[index].value;
if (input === 'select') {
return (
<SelectFilter
Header={Header}
emptyLabel={unfilteredLabel}
fetchSelects={fetchSelects}
initialValue={initialValue}
key={id}
name={id}
onSelect={(value: any) => updateFilterValue(index, value)}
onSelect={(option: SelectOption | undefined) =>
updateFilterValue(index, option)
}
paginate={paginate}
selects={selects}
/>
@ -100,7 +87,7 @@ function UIFilters({
return null;
},
)}
</FilterWrapper>
</>
);
}

View File

@ -377,8 +377,8 @@ describe('ListView', () => {
expect(wrapper.find(ListViewFilters)).toExist();
});
it('fetched async filter values on mount', () => {
expect(fetchSelectsMock).toHaveBeenCalled();
it('does not fetch async filter values on mount', () => {
expect(fetchSelectsMock).not.toHaveBeenCalled();
});
it('calls fetchData on filter', () => {
@ -387,7 +387,7 @@ describe('ListView', () => {
.find('[data-test="filters-select"]')
.first()
.props()
.onChange({ value: 'bar' });
.onChange({ label: 'bar', value: 'bar' });
});
act(() => {
@ -395,13 +395,15 @@ describe('ListView', () => {
.find('[data-test="filters-search"]')
.first()
.props()
.onChange({ currentTarget: { value: 'something' } });
.onChange({
currentTarget: { label: 'something', value: 'something' },
});
});
wrapper.update();
act(() => {
wrapper.find('[data-test="search-input"]').last().props().onBlur();
wrapper.find('[data-test="filters-search"]').last().props().onBlur();
});
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
@ -411,7 +413,10 @@ describe('ListView', () => {
Object {
"id": "id",
"operator": "eq",
"value": "bar",
"value": Object {
"label": "bar",
"value": "bar",
},
},
],
"pageIndex": 0,
@ -433,7 +438,10 @@ describe('ListView', () => {
Object {
"id": "id",
"operator": "eq",
"value": "bar",
"value": Object {
"label": "bar",
"value": "bar",
},
},
Object {
"id": "name",
@ -462,7 +470,7 @@ describe('ListView', () => {
});
await act(async () => {
wrapper2.find('[data-test="card-sort-select"]').first().props().onChange({
wrapper2.find('[aria-label="Sort"]').first().props().onChange({
desc: false,
id: 'something',
label: 'Alphabetical',

View File

@ -50,13 +50,11 @@ const ListViewStyles = styled.div`
display: flex;
padding-bottom: ${({ theme }) => theme.gridUnit * 4}px;
.header-left {
& .controls {
display: flex;
flex: 5;
}
.header-right {
flex: 1;
text-align: right;
flex-wrap: wrap;
column-gap: ${({ theme }) => theme.gridUnit * 6}px;
row-gap: ${({ theme }) => theme.gridUnit * 4}px;
}
}
@ -136,6 +134,7 @@ const bulkSelectColumnConfig = {
const ViewModeContainer = styled.div`
padding-right: ${({ theme }) => theme.gridUnit * 4}px;
margin-top: ${({ theme }) => theme.gridUnit * 5 + 1}px;
display: inline-block;
.toggle-button {
@ -301,10 +300,10 @@ function ListView<T extends object = any>({
<ListViewStyles>
<div data-test={className} className={`superset-list-view ${className}`}>
<div className="header">
<div className="header-left">
{cardViewEnabled && (
<ViewModeToggle mode={viewMode} setMode={setViewMode} />
)}
{cardViewEnabled && (
<ViewModeToggle mode={viewMode} setMode={setViewMode} />
)}
<div className="controls">
{filterable && (
<FilterControls
filters={filters}
@ -312,8 +311,6 @@ function ListView<T extends object = any>({
updateFilterValue={applyFilterValue}
/>
)}
</div>
<div className="header-right">
{viewMode === 'card' && cardSortSelectOptions && (
<CardSortSelect
initialSort={initialSort}

View File

@ -53,10 +53,10 @@ export interface Filter {
selects?: SelectOption[];
onFilterOpen?: () => void;
fetchSelects?: (
filterValue?: string,
pageIndex?: number,
pageSize?: number,
) => Promise<SelectOption[]>;
filterValue: string,
page: number,
pageSize: number,
) => Promise<{ data: SelectOption[]; totalCount: number }>;
paginate?: boolean;
}
@ -68,7 +68,15 @@ export interface FilterValue {
id: string;
urlDisplay?: string;
operator?: string;
value: string | boolean | number | null | undefined | string[] | number[];
value:
| string
| boolean
| number
| null
| undefined
| string[]
| number[]
| { label: string; value: string | number };
}
export interface FetchDataConfig {

View File

@ -55,6 +55,8 @@ const RisonParam: QueryParamConfig<string, any> = {
: rison.decode(dataStr),
};
export const SELECT_WIDTH = 200;
export class ListViewError extends Error {
name = 'ListViewError';
}

View File

@ -1,62 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import SearchInput, { SearchInputProps } from '.';
export default {
title: 'SearchInput',
component: SearchInput,
};
export const InteractiveSearchInput = ({
value,
...rest
}: SearchInputProps) => {
const [currentValue, setCurrentValue] = useState(value);
return (
<div style={{ width: 230 }}>
<SearchInput
{...rest}
value={currentValue}
onChange={e => setCurrentValue(e.target.value)}
onClear={() => setCurrentValue('')}
/>
</div>
);
};
InteractiveSearchInput.args = {
value: 'Test',
placeholder: 'Enter some text',
name: 'search-input',
};
InteractiveSearchInput.argTypes = {
onSubmit: { action: 'onSubmit' },
onClear: { action: 'onClear' },
onChange: { action: 'onChange' },
};
InteractiveSearchInput.story = {
parameters: {
knobs: {
disable: true,
},
},
};

View File

@ -1,93 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
import SearchInput from 'src/components/SearchInput';
describe('SearchInput', () => {
const defaultProps = {
onSubmit: jest.fn(),
onClear: jest.fn(),
onChange: jest.fn(),
value: '',
};
const factory = overrideProps => {
const props = { ...defaultProps, ...(overrideProps || {}) };
return mount(
<ThemeProvider theme={supersetTheme}>
<SearchInput {...props} />
</ThemeProvider>,
);
};
let wrapper;
beforeAll(() => {
wrapper = factory();
});
afterEach(() => {
defaultProps.onSubmit.mockClear();
defaultProps.onClear.mockClear();
defaultProps.onChange.mockClear();
});
it('renders', () => {
expect(React.isValidElement(<SearchInput {...defaultProps} />)).toBe(true);
});
const typeSearchInput = value => {
wrapper
.find('[data-test="search-input"]')
.first()
.props()
.onChange({ currentTarget: { value } });
};
it('submits on enter', () => {
typeSearchInput('foo');
wrapper
.find('[data-test="search-input"]')
.first()
.props()
.onKeyDown({ key: 'Enter' });
expect(defaultProps.onChange).toHaveBeenCalled();
expect(defaultProps.onSubmit).toHaveBeenCalled();
});
it('submits on search icon click', () => {
typeSearchInput('bar');
wrapper.find('[data-test="search-submit"]').first().props().onClick();
expect(defaultProps.onSubmit).toHaveBeenCalled();
});
it('clears on clear icon click', () => {
const wrapper2 = factory({ value: 'fizz' });
wrapper2.find('[data-test="search-clear"]').first().props().onClick();
expect(defaultProps.onClear).toHaveBeenCalled();
});
});

View File

@ -1,108 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { styled, useTheme } from '@superset-ui/core';
import React from 'react';
import Icons from 'src/components/Icons';
export interface SearchInputProps {
onSubmit: () => void;
onClear: () => void;
value: string;
onChange: React.EventHandler<React.ChangeEvent<HTMLInputElement>>;
placeholder?: string;
name?: string;
}
const SearchInputWrapper = styled.div`
position: relative;
`;
const StyledInput = styled.input`
width: 200px;
height: ${({ theme }) => theme.gridUnit * 8}px;
background-image: none;
border: 1px solid ${({ theme }) => theme.colors.secondary.light2};
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
padding: 4px 28px;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
outline: none;
}
`;
const commonStyles = `
position: absolute;
z-index: 2;
display: block;
cursor: pointer;
`;
const SearchIcon = styled(Icons.Search)`
${commonStyles};
top: 4px;
left: 2px;
`;
const ClearIcon = styled(Icons.CancelX)`
${commonStyles};
right: 0px;
top: 4px;
`;
export default function SearchInput({
onChange,
onClear,
onSubmit,
placeholder = 'Search',
name,
value,
}: SearchInputProps) {
const theme = useTheme();
return (
<SearchInputWrapper>
<SearchIcon
iconColor={theme.colors.grayscale.base}
data-test="search-submit"
role="button"
onClick={() => onSubmit()}
/>
<StyledInput
data-test="search-input"
onKeyDown={e => {
if (e.key === 'Enter') {
onSubmit();
}
}}
onBlur={() => onSubmit()}
placeholder={placeholder}
onChange={onChange}
value={value}
name={name}
/>
{value && (
<ClearIcon
data-test="search-clear"
role="button"
iconColor={theme.colors.grayscale.base}
onClick={() => onClear()}
/>
)}
</SearchInputWrapper>
);
}

View File

@ -56,6 +56,8 @@ interface AlertListProps {
isReportEnabled: boolean;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
const deleteAlerts = makeApi<number[], { message: string }>({
@ -381,7 +383,7 @@ function AlertList({
createErrorHandler(errMsg =>
t('An error occurred while fetching created by values: %s', errMsg),
),
user.userId,
user,
),
paginate: true,
},

View File

@ -46,6 +46,8 @@ interface AnnotationLayersListProps {
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
@ -301,7 +303,7 @@ function AnnotationLayersList({
errMsg,
),
),
user.userId,
user,
),
paginate: true,
},

View File

@ -77,42 +77,40 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
setupPlugins();
const registry = getChartMetadataRegistry();
const createFetchDatasets = (handleError: (err: Response) => void) => async (
const createFetchDatasets = async (
filterValue = '',
pageIndex?: number,
pageSize?: number,
page: number,
pageSize: number,
) => {
// add filters if filterValue
const filters = filterValue
? { filters: [{ col: 'table_name', opr: 'sw', value: filterValue }] }
: {};
try {
const queryParams = rison.encode({
columns: ['datasource_name', 'datasource_id'],
keys: ['none'],
order_column: 'table_name',
order_direction: 'asc',
...(pageIndex ? { page: pageIndex } : {}),
...(pageSize ? { page_size: pageSize } : {}),
...filters,
});
const queryParams = rison.encode({
columns: ['datasource_name', 'datasource_id'],
keys: ['none'],
order_column: 'table_name',
order_direction: 'asc',
page,
page_size: pageSize,
...filters,
});
const { json = {} } = await SupersetClient.get({
endpoint: `/api/v1/dataset/?q=${queryParams}`,
});
const { json = {} } = await SupersetClient.get({
endpoint: `/api/v1/dataset/?q=${queryParams}`,
});
const datasets = json?.result?.map(
({ table_name: tableName, id }: { table_name: string; id: number }) => ({
label: tableName,
value: id,
}),
);
const datasets = json?.result?.map(
({ table_name: tableName, id }: { table_name: string; id: number }) => ({
label: tableName,
value: id,
}),
);
return uniqBy<SelectOption>(datasets, 'value');
} catch (e) {
handleError(e);
}
return [];
return {
data: uniqBy<SelectOption>(datasets, 'value'),
totalCount: json?.count,
};
};
interface ChartListProps {
@ -120,6 +118,8 @@ interface ChartListProps {
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
@ -417,113 +417,110 @@ function ChartList(props: ChartListProps) {
],
);
const favoritesFilter: Filter = {
Header: t('Favorite'),
id: 'id',
urlDisplay: 'favorite',
input: 'select',
operator: FilterOperator.chartIsFav,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Yes'), value: true },
{ label: t('No'), value: false },
],
};
const favoritesFilter: Filter = useMemo(
() => ({
Header: t('Favorite'),
id: 'id',
urlDisplay: 'favorite',
input: 'select',
operator: FilterOperator.chartIsFav,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Yes'), value: true },
{ label: t('No'), value: false },
],
}),
[],
);
const filters: Filters = [
{
Header: t('Owner'),
id: 'owners',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart owners values: %s',
errMsg,
const filters: Filters = useMemo(
() => [
{
Header: t('Owner'),
id: 'owners',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart owners values: %s',
errMsg,
),
),
),
props.user,
),
props.user.userId,
),
paginate: true,
},
{
Header: t('Created by'),
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart created by values: %s',
errMsg,
paginate: true,
},
{
Header: t('Created by'),
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart created by values: %s',
errMsg,
),
),
),
props.user,
),
props.user.userId,
),
paginate: true,
},
{
Header: t('Viz type'),
id: 'viz_type',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('All'),
selects: registry
.keys()
.filter(k => nativeFilterGate(registry.get(k)?.behaviors || []))
.map(k => ({ label: registry.get(k)?.name || k, value: k }))
.sort((a, b) => {
if (!a.label || !b.label) {
paginate: true,
},
{
Header: t('Viz type'),
id: 'viz_type',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('All'),
selects: registry
.keys()
.filter(k => nativeFilterGate(registry.get(k)?.behaviors || []))
.map(k => ({ label: registry.get(k)?.name || k, value: k }))
.sort((a, b) => {
if (!a.label || !b.label) {
return 0;
}
if (a.label > b.label) {
return 1;
}
if (a.label < b.label) {
return -1;
}
return 0;
}
if (a.label > b.label) {
return 1;
}
if (a.label < b.label) {
return -1;
}
return 0;
}),
},
{
Header: t('Dataset'),
id: 'datasource_id',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('All'),
fetchSelects: createFetchDatasets(
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart dataset values: %s',
errMsg,
),
),
),
),
paginate: true,
},
...(props.user.userId ? [favoritesFilter] : []),
{
Header: t('Search'),
id: 'slice_name',
input: 'search',
operator: FilterOperator.chartAllText,
},
];
}),
},
{
Header: t('Dataset'),
id: 'datasource_id',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('All'),
fetchSelects: createFetchDatasets,
paginate: true,
},
...(props.user.userId ? [favoritesFilter] : []),
{
Header: t('Search'),
id: 'slice_name',
input: 'search',
operator: FilterOperator.chartAllText,
},
],
[addDangerToast, favoritesFilter, props.user],
);
const sortTypes = [
{

View File

@ -45,6 +45,8 @@ interface CssTemplatesListProps {
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
@ -287,7 +289,7 @@ function CssTemplatesList({
errMsg,
),
),
user.userId,
user,
),
paginate: true,
},

View File

@ -71,6 +71,8 @@ interface DashboardListProps {
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
@ -404,81 +406,87 @@ function DashboardList(props: DashboardListProps) {
],
);
const favoritesFilter: Filter = {
Header: t('Favorite'),
id: 'id',
urlDisplay: 'favorite',
input: 'select',
operator: FilterOperator.dashboardIsFav,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Yes'), value: true },
{ label: t('No'), value: false },
],
};
const filters: Filters = [
{
Header: t('Owner'),
id: 'owners',
const favoritesFilter: Filter = useMemo(
() => ({
Header: t('Favorite'),
id: 'id',
urlDisplay: 'favorite',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dashboard',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching dashboard owner values: %s',
errMsg,
),
),
),
props.user.userId,
),
paginate: true,
},
{
Header: t('Created by'),
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dashboard',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching dashboard created by values: %s',
errMsg,
),
),
),
props.user.userId,
),
paginate: true,
},
{
Header: t('Status'),
id: 'published',
input: 'select',
operator: FilterOperator.equals,
operator: FilterOperator.dashboardIsFav,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Published'), value: true },
{ label: t('Draft'), value: false },
{ label: t('Yes'), value: true },
{ label: t('No'), value: false },
],
},
...(props.user.userId ? [favoritesFilter] : []),
{
Header: t('Search'),
id: 'dashboard_title',
input: 'search',
operator: FilterOperator.titleOrSlug,
},
];
}),
[],
);
const filters: Filters = useMemo(
() => [
{
Header: t('Owner'),
id: 'owners',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dashboard',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching dashboard owner values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Created by'),
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dashboard',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching dashboard created by values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Status'),
id: 'published',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Published'), value: true },
{ label: t('Draft'), value: false },
],
},
...(props.user.userId ? [favoritesFilter] : []),
{
Header: t('Search'),
id: 'dashboard_title',
input: 'search',
operator: FilterOperator.titleOrSlug,
},
],
[addDangerToast, favoritesFilter, props.user],
);
const sortTypes = [
{

View File

@ -161,13 +161,13 @@ describe('DatabaseList', () => {
.find('[name="expose_in_sqllab"]')
.first()
.props()
.onSelect(true);
.onSelect({ label: 'Yes', value: true });
filtersWrapper
.find('[name="allow_run_async"]')
.first()
.props()
.onSelect(false);
.onSelect({ label: 'Yes', value: false });
filtersWrapper
.find('[name="database_name"]')

View File

@ -118,12 +118,14 @@ describe('DatasetList', () => {
);
});
it('fetches owner filter values', () => {
expect(fetchMock.calls(/dataset\/related\/owners/)).toHaveLength(1);
it('does not fetch owner filter values on mount', async () => {
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/dataset\/related\/owners/)).toHaveLength(0);
});
it('fetches schema filter values', () => {
expect(fetchMock.calls(/dataset\/distinct\/schema/)).toHaveLength(1);
it('does not fetch schema filter values on mount', async () => {
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/dataset\/distinct\/schema/)).toHaveLength(0);
});
it('shows/hides bulk actions when bulk actions is clicked', async () => {

View File

@ -98,6 +98,8 @@ interface DatasetListProps {
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
@ -414,7 +416,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
errMsg,
),
),
user.userId,
user,
),
paginate: true,
},

View File

@ -374,7 +374,7 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
'user',
createErrorHandler(errMsg =>
addDangerToast(
t('An error occurred while fetching database values: %s', errMsg),
t('An error occurred while fetching user values: %s', errMsg),
),
),
),

View File

@ -271,7 +271,7 @@ describe('RTL', () => {
it('renders an export button in the actions bar', async () => {
// Grab Export action button and mock mouse hovering over it
const exportActionButton = screen.getAllByRole('button')[18];
const exportActionButton = screen.getAllByTestId('export-action')[0];
userEvent.hover(exportActionButton);
// Wait for the tooltip to pop up

View File

@ -141,7 +141,10 @@ export function useListViewResource<D extends object = any>(
.map(({ id, operator: opr, value }) => ({
col: id,
opr,
value,
value:
value && typeof value === 'object' && 'value' in value
? value.value
: value,
}));
const queryParams = rison.encode({

View File

@ -37,32 +37,52 @@ const createFetchResourceMethod = (method: string) => (
resource: string,
relation: string,
handleError: (error: Response) => void,
userId?: string | number,
) => async (filterValue = '', pageIndex?: number, pageSize?: number) => {
user?: { userId: string | number; firstName: string; lastName: string },
) => async (filterValue = '', page: number, pageSize: number) => {
const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`;
const options =
userId && pageIndex === 0 ? [{ label: 'me', value: userId }] : [];
try {
const queryParams = rison.encode({
...(pageIndex ? { page: pageIndex } : {}),
...(pageSize ? { page_size: pageSize } : {}),
...(filterValue ? { filter: filterValue } : {}),
});
const { json = {} } = await SupersetClient.get({
endpoint: `${resourceEndpoint}?q=${queryParams}`,
});
const data = json?.result?.map(
({ text: label, value }: { text: string; value: any }) => ({
label,
value,
}),
);
const queryParams = rison.encode({
filter: filterValue,
page,
page_size: pageSize,
});
const { json = {} } = await SupersetClient.get({
endpoint: `${resourceEndpoint}?q=${queryParams}`,
});
return options.concat(data);
} catch (e) {
handleError(e);
let fetchedLoggedUser = false;
const loggedUser = user
? {
label: `${user.firstName} ${user.lastName}`,
value: user.userId,
}
: undefined;
const data: { label: string; value: string | number }[] = [];
json?.result?.forEach(
({ text, value }: { text: string; value: string | number }) => {
if (
loggedUser &&
value === loggedUser.value &&
text === loggedUser.label
) {
fetchedLoggedUser = true;
} else {
data.push({
label: text,
value,
});
}
},
);
if (loggedUser && (!filterValue || fetchedLoggedUser)) {
data.unshift(loggedUser);
}
return [];
return {
data,
totalCount: json?.count,
};
};
export const PAGE_SIZE = 5;