mirror of
https://github.com/apache/superset.git
synced 2024-09-12 16:49:40 -04:00
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:
parent
596e1cdf9b
commit
b6d78bf4f2
@ -27,44 +27,34 @@ describe('chart card view filters', () => {
|
|||||||
|
|
||||||
it('should filter by owners correctly', () => {
|
it('should filter by owners correctly', () => {
|
||||||
// filter by owners
|
// filter by owners
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="styled-card"]').should('not.exist');
|
cy.get('[data-test="styled-card"]').should('not.exist');
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="styled-card"]').should('not.exist');
|
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', () => {
|
it('should filter by created by correctly', () => {
|
||||||
// filter by created by
|
// filter by created by
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('.ant-card').should('not.exist');
|
cy.get('.ant-card').should('not.exist');
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="styled-card"]').should('not.exist');
|
cy.get('[data-test="styled-card"]').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
xit('should filter by viz type correctly', () => {
|
xit('should filter by viz type correctly', () => {
|
||||||
// filter by viz type
|
// filter by viz type
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__menu').contains('area').click({ timeout: 5000 });
|
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"]').its('length').should('be.gt', 0);
|
||||||
cy.get('[data-test="styled-card"]')
|
cy.get('[data-test="styled-card"]')
|
||||||
.contains("World's Pop Growth")
|
.contains("World's Pop Growth")
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__control').eq(2).type('world_map{enter}');
|
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"]').should('have.length', 1);
|
||||||
cy.get('[data-test="styled-card"]')
|
cy.get('[data-test="styled-card"]')
|
||||||
.contains('% Rural')
|
.contains('% Rural')
|
||||||
@ -73,14 +63,16 @@ describe('chart card view filters', () => {
|
|||||||
|
|
||||||
it('should filter by datasource correctly', () => {
|
it('should filter by datasource correctly', () => {
|
||||||
// filter by datasource
|
// filter by datasource
|
||||||
cy.get('.Select__control').eq(3).click();
|
cy.get('[data-test="filters-select"]').eq(3).click();
|
||||||
cy.get('.Select__menu').contains('unicode_test').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"]').should('have.length', 1);
|
||||||
cy.get('[data-test="styled-card"]')
|
cy.get('[data-test="styled-card"]')
|
||||||
.contains('Unicode Cloud')
|
.contains('Unicode Cloud')
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__control').eq(2).type('energy_usage{enter}{enter}');
|
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);
|
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', () => {
|
it('should filter by owners correctly', () => {
|
||||||
// filter by owners
|
// filter by owners
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
cy.get('[data-test="table-row"]').should('not.exist');
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
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', () => {
|
it('should filter by created by correctly', () => {
|
||||||
// filter by created by
|
// filter by created by
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
cy.get('[data-test="table-row"]').should('not.exist');
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
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
|
// this is flaky, but seems to fail along with the card view test of the same name
|
||||||
xit('should filter by viz type correctly', () => {
|
xit('should filter by viz type correctly', () => {
|
||||||
// filter by viz type
|
// filter by viz type
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__menu').contains('area').click({ timeout: 5000 });
|
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"]').its('length').should('be.gt', 0);
|
||||||
cy.get('[data-test="table-row"]')
|
cy.get('[data-test="table-row"]')
|
||||||
.contains("World's Pop Growth")
|
.contains("World's Pop Growth")
|
||||||
.should('exist');
|
.should('exist');
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__control').eq(2).type('world_map{enter}');
|
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"]').should('have.length', 1);
|
||||||
cy.get('[data-test="table-row"]').contains('% Rural').should('exist');
|
cy.get('[data-test="table-row"]').contains('% Rural').should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter by datasource correctly', () => {
|
it('should filter by datasource correctly', () => {
|
||||||
// filter by datasource
|
// filter by datasource
|
||||||
cy.get('.Select__control').eq(3).click();
|
cy.get('[data-test="filters-select"]').eq(3).click();
|
||||||
cy.get('.Select__menu').contains('unicode_test').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"]').should('have.length', 1);
|
||||||
cy.get('[data-test="table-row"]').contains('Unicode Cloud').should('exist');
|
cy.get('[data-test="table-row"]').contains('Unicode Cloud').should('exist');
|
||||||
cy.get('.Select__control').eq(3).click();
|
cy.get('[data-test="filters-select"]').eq(3).click();
|
||||||
cy.get('.Select__control').eq(3).type('energy_usage{enter}{enter}');
|
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);
|
cy.get('[data-test="table-row"]').its('length').should('be.gt', 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,44 +27,34 @@ describe('dashboard filters card view', () => {
|
|||||||
|
|
||||||
it('should filter by owners correctly', () => {
|
it('should filter by owners correctly', () => {
|
||||||
// filter by owners
|
// filter by owners
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="styled-card"]').should('not.exist');
|
cy.get('[data-test="styled-card"]').should('not.exist');
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="styled-card"]').should('not.exist');
|
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', () => {
|
it('should filter by created by correctly', () => {
|
||||||
// filter by created by
|
// filter by created by
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('.ant-card').should('not.exist');
|
cy.get('.ant-card').should('not.exist');
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('.ant-card').should('not.exist');
|
cy.get('.ant-card').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter by published correctly', () => {
|
it('should filter by published correctly', () => {
|
||||||
// filter by published
|
// filter by published
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__menu').contains('Published').click({ timeout: 5000 });
|
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"]').should('have.length', 3);
|
||||||
cy.get('[data-test="styled-card"]')
|
cy.get('[data-test="styled-card"]')
|
||||||
.contains('USA Births Names')
|
.contains('USA Births Names')
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__control').eq(1).type('unpub{enter}');
|
cy.get('[data-test="filters-select"]').eq(1).type('unpub{enter}');
|
||||||
cy.get('[data-test="styled-card"]').should('have.length', 3);
|
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', () => {
|
it('should filter by owners correctly', () => {
|
||||||
// filter by owners
|
// filter by owners
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
cy.get('[data-test="table-row"]').should('not.exist');
|
||||||
cy.get('.Select__control').first().click();
|
cy.get('[data-test="filters-select"]').first().click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
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', () => {
|
it('should filter by created by correctly', () => {
|
||||||
// filter by created by
|
// filter by created by
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('alpha user').click();
|
cy.get('.rc-virtual-list').contains('alpha user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
cy.get('[data-test="table-row"]').should('not.exist');
|
||||||
cy.get('.Select__control').eq(1).click();
|
cy.get('[data-test="filters-select"]').eq(1).click();
|
||||||
cy.get('.Select__menu').contains('gamma user').click();
|
cy.get('.rc-virtual-list').contains('gamma user').click();
|
||||||
cy.get('[data-test="table-row"]').should('not.exist');
|
cy.get('[data-test="table-row"]').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter by published correctly', () => {
|
it('should filter by published correctly', () => {
|
||||||
// filter by published
|
// filter by published
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__menu').contains('Published').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"]').should('have.length', 3);
|
||||||
cy.get('[data-test="table-row"]')
|
cy.get('[data-test="table-row"]')
|
||||||
.contains('USA Births Names')
|
.contains('USA Births Names')
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
cy.get('.Select__control').eq(2).click();
|
cy.get('[data-test="filters-select"]').eq(2).click();
|
||||||
cy.get('.Select__control').eq(2).type('unpub{enter}');
|
cy.get('[data-test="filters-select"]').eq(2).type('unpub{enter}');
|
||||||
cy.get('[data-test="table-row"]').should('have.length', 3);
|
cy.get('[data-test="table-row"]').should('have.length', 3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { DatePicker as AntdDatePicker } from 'antd';
|
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;
|
export const DatePicker = AntdDatePicker;
|
||||||
|
@ -16,24 +16,21 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { styled, withTheme, SupersetThemeProps, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { PartialThemeConfig, Select } from 'src/components/Select';
|
import { Select } from 'src/components';
|
||||||
|
import { FormLabel } from 'src/components/Form';
|
||||||
|
import { SELECT_WIDTH } from './utils';
|
||||||
import { CardSortSelectOption, FetchDataConfig, SortColumn } from './types';
|
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`
|
const SortContainer = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||||
padding-top: ${({ theme }) => theme.gridUnit}px;
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
width: ${SELECT_WIDTH}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface CardViewSelectSortProps {
|
interface CardViewSelectSortProps {
|
||||||
onChange: (conf: FetchDataConfig) => any;
|
onChange: (conf: FetchDataConfig) => any;
|
||||||
options: Array<CardSortSelectOption>;
|
options: Array<CardSortSelectOption>;
|
||||||
@ -42,43 +39,6 @@ interface CardViewSelectSortProps {
|
|||||||
pageSize: number;
|
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 = ({
|
export const CardSortSelect = ({
|
||||||
initialSort,
|
initialSort,
|
||||||
onChange,
|
onChange,
|
||||||
@ -87,25 +47,45 @@ export const CardSortSelect = ({
|
|||||||
pageSize,
|
pageSize,
|
||||||
}: CardViewSelectSortProps) => {
|
}: CardViewSelectSortProps) => {
|
||||||
const defaultSort =
|
const defaultSort =
|
||||||
initialSort && options.find(({ id }) => id === initialSort[0].id);
|
(initialSort && options.find(({ id }) => id === initialSort[0].id)) ||
|
||||||
const [selectedOption, setSelectedOption] = useState<CardSortSelectOption>(
|
options[0];
|
||||||
defaultSort || 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) => {
|
const handleOnChange = (selected: { label: string; value: string }) => {
|
||||||
setSelectedOption(selected);
|
setValue(selected);
|
||||||
const sortBy = [{ id: selected.id, desc: selected.desc }];
|
const originalOption = options.find(
|
||||||
onChange({ pageIndex, pageSize, sortBy, filters: [] });
|
({ value }) => value === selected.value,
|
||||||
|
);
|
||||||
|
if (originalOption) {
|
||||||
|
const sortBy = [
|
||||||
|
{
|
||||||
|
id: originalOption.id,
|
||||||
|
desc: originalOption.desc,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
onChange({ pageIndex, pageSize, sortBy, filters: [] });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortContainer>
|
<SortContainer>
|
||||||
<SortTitle>{t('Sort:')}</SortTitle>
|
<Select
|
||||||
<StyledCardSortSelect
|
ariaLabel={t('Sort')}
|
||||||
|
header={<FormLabel>{t('Sort')}</FormLabel>}
|
||||||
|
labelInValue
|
||||||
onChange={(value: CardSortSelectOption) => handleOnChange(value)}
|
onChange={(value: CardSortSelectOption) => handleOnChange(value)}
|
||||||
options={options}
|
options={formattedOptions}
|
||||||
selectStyles={filterSelectStyles}
|
showSearch
|
||||||
value={selectedOption}
|
value={value}
|
||||||
/>
|
/>
|
||||||
</SortContainer>
|
</SortContainer>
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { styled } from '@superset-ui/core';
|
import { styled } from '@superset-ui/core';
|
||||||
|
import { SELECT_WIDTH } from 'src/components/ListView/utils';
|
||||||
|
|
||||||
export interface BaseFilter {
|
export interface BaseFilter {
|
||||||
Header: ReactNode;
|
Header: ReactNode;
|
||||||
@ -26,12 +27,7 @@ export interface BaseFilter {
|
|||||||
|
|
||||||
export const FilterContainer = styled.div`
|
export const FilterContainer = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-right: 2em;
|
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
width: ${SELECT_WIDTH}px;
|
||||||
|
|
||||||
export const FilterTitle = styled.label`
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0.4em 0 0;
|
|
||||||
`;
|
`;
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import moment, { Moment } from 'moment';
|
import moment, { Moment } from 'moment';
|
||||||
import { styled } from '@superset-ui/core';
|
import { styled } from '@superset-ui/core';
|
||||||
import { RangePicker as AntRangePicker } from 'src/components/DatePicker';
|
import { RangePicker } from 'src/components/DatePicker';
|
||||||
import { FilterContainer, BaseFilter, FilterTitle } from './Base';
|
import { FormLabel } from 'src/components/Form';
|
||||||
|
import { BaseFilter } from './Base';
|
||||||
|
|
||||||
interface DateRangeFilterProps extends BaseFilter {
|
interface DateRangeFilterProps extends BaseFilter {
|
||||||
onSubmit: (val: number[]) => void;
|
onSubmit: (val: number[]) => void;
|
||||||
@ -29,13 +30,12 @@ interface DateRangeFilterProps extends BaseFilter {
|
|||||||
|
|
||||||
type ValueState = [number, number];
|
type ValueState = [number, number];
|
||||||
|
|
||||||
const RangePicker = styled(AntRangePicker)`
|
const RangeFilterContainer = styled.div`
|
||||||
padding: 0 11px;
|
display: inline-flex;
|
||||||
transform: translateX(-7px);
|
flex-direction: column;
|
||||||
`;
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
const RangeFilterContainer = styled(FilterContainer)`
|
width: 360px;
|
||||||
margin-right: 1em;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function DateRangeFilter({
|
export default function DateRangeFilter({
|
||||||
@ -51,10 +51,9 @@ export default function DateRangeFilter({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RangeFilterContainer>
|
<RangeFilterContainer>
|
||||||
<FilterTitle>{Header}:</FilterTitle>
|
<FormLabel>{Header}</FormLabel>
|
||||||
<RangePicker
|
<RangePicker
|
||||||
showTime
|
showTime
|
||||||
bordered={false}
|
|
||||||
value={momentValue}
|
value={momentValue}
|
||||||
onChange={momentRange => {
|
onChange={momentRange => {
|
||||||
if (!momentRange) {
|
if (!momentRange) {
|
||||||
|
@ -17,8 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import SearchInput from 'src/components/SearchInput';
|
import { t, styled } from '@superset-ui/core';
|
||||||
import { FilterContainer, BaseFilter } from './Base';
|
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 {
|
interface SearchHeaderProps extends BaseFilter {
|
||||||
Header: string;
|
Header: string;
|
||||||
@ -26,6 +30,18 @@ interface SearchHeaderProps extends BaseFilter {
|
|||||||
name: string;
|
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({
|
export default function SearchFilter({
|
||||||
Header,
|
Header,
|
||||||
name,
|
name,
|
||||||
@ -38,28 +54,27 @@ export default function SearchFilter({
|
|||||||
onSubmit(value.trim());
|
onSubmit(value.trim());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onClear = () => {
|
|
||||||
setValue('');
|
|
||||||
onSubmit('');
|
|
||||||
};
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(e.currentTarget.value);
|
setValue(e.currentTarget.value);
|
||||||
if (e.currentTarget.value === '') {
|
if (e.currentTarget.value === '') {
|
||||||
onClear();
|
onSubmit('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterContainer>
|
<Container>
|
||||||
<SearchInput
|
<FormLabel>{Header}</FormLabel>
|
||||||
|
<StyledInput
|
||||||
|
allowClear
|
||||||
data-test="filters-search"
|
data-test="filters-search"
|
||||||
placeholder={Header}
|
placeholder={t('Type a value')}
|
||||||
name={name}
|
name={name}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={handleSubmit}
|
onPressEnter={handleSubmit}
|
||||||
onClear={onClear}
|
onBlur={handleSubmit}
|
||||||
|
prefix={<SearchIcon iconSize="l" />}
|
||||||
/>
|
/>
|
||||||
</FilterContainer>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,138 +16,76 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { withTheme, SupersetThemeProps } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import {
|
import { Select } from 'src/components';
|
||||||
Select,
|
|
||||||
PaginatedSelect,
|
|
||||||
PartialThemeConfig,
|
|
||||||
} from 'src/components/Select';
|
|
||||||
import { Filter, SelectOption } from 'src/components/ListView/types';
|
import { Filter, SelectOption } from 'src/components/ListView/types';
|
||||||
import { filterSelectStyles } from 'src/components/ListView/utils';
|
import { FormLabel } from 'src/components/Form';
|
||||||
import { FilterContainer, BaseFilter, FilterTitle } from './Base';
|
import { FilterContainer, BaseFilter } from './Base';
|
||||||
|
|
||||||
interface SelectFilterProps extends BaseFilter {
|
interface SelectFilterProps extends BaseFilter {
|
||||||
emptyLabel?: string;
|
|
||||||
fetchSelects?: Filter['fetchSelects'];
|
fetchSelects?: Filter['fetchSelects'];
|
||||||
name?: string;
|
name?: string;
|
||||||
onSelect: (selected: any) => any;
|
onSelect: (selected: SelectOption | undefined) => void;
|
||||||
paginate?: boolean;
|
paginate?: boolean;
|
||||||
selects: Filter['selects'];
|
selects: Filter['selects'];
|
||||||
theme: SupersetThemeProps['theme'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
|
|
||||||
|
|
||||||
function SelectFilter({
|
function SelectFilter({
|
||||||
Header,
|
Header,
|
||||||
emptyLabel = 'None',
|
name,
|
||||||
fetchSelects,
|
fetchSelects,
|
||||||
initialValue,
|
initialValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
paginate = false,
|
|
||||||
selects = [],
|
selects = [],
|
||||||
theme,
|
|
||||||
}: SelectFilterProps) {
|
}: SelectFilterProps) {
|
||||||
const filterSelectTheme: PartialThemeConfig = {
|
const [selectedOption, setSelectedOption] = useState(initialValue);
|
||||||
spacing: {
|
|
||||||
baseUnit: 2,
|
|
||||||
fontSize: theme.typography.sizes.s,
|
|
||||||
minWidth: '5em',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilterSelect = {
|
const onChange = (selected: SelectOption) => {
|
||||||
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;
|
|
||||||
onSelect(
|
onSelect(
|
||||||
selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value,
|
selected ? { label: selected.label, value: selected.value } : undefined,
|
||||||
);
|
);
|
||||||
setSelectedOption(selected);
|
setSelectedOption(selected);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAndFormatSelects = async (
|
const onClear = () => {
|
||||||
inputValue: string,
|
onSelect(undefined);
|
||||||
loadedOptions: SelectOption[],
|
setSelectedOption(undefined);
|
||||||
{ 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 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 (
|
return (
|
||||||
<FilterContainer>
|
<FilterContainer>
|
||||||
<FilterTitle>{Header}:</FilterTitle>
|
<Select
|
||||||
{fetchSelects ? (
|
allowClear
|
||||||
<PaginatedSelect
|
ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
|
||||||
data-test="filters-select"
|
labelInValue
|
||||||
defaultOptions
|
data-test="filters-select"
|
||||||
themeConfig={filterSelectTheme}
|
header={<FormLabel>{Header}</FormLabel>}
|
||||||
stylesConfig={filterSelectStyles}
|
onChange={onChange}
|
||||||
// @ts-ignore
|
onClear={onClear}
|
||||||
value={selectedOption}
|
options={fetchSelects ? fetchAndFormatSelects : selects}
|
||||||
// @ts-ignore
|
placeholder={t('Select or type a value')}
|
||||||
onChange={onChange}
|
showSearch
|
||||||
// @ts-ignore
|
value={selectedOption}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FilterContainer>
|
</FilterContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default withTheme(SelectFilter);
|
export default SelectFilter;
|
||||||
|
@ -17,12 +17,13 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { styled, withTheme } from '@superset-ui/core';
|
import { withTheme } from '@superset-ui/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FilterValue,
|
FilterValue,
|
||||||
Filters,
|
Filters,
|
||||||
InternalFilter,
|
InternalFilter,
|
||||||
|
SelectOption,
|
||||||
} from 'src/components/ListView/types';
|
} from 'src/components/ListView/types';
|
||||||
import SearchFilter from './Search';
|
import SearchFilter from './Search';
|
||||||
import SelectFilter from './Select';
|
import SelectFilter from './Select';
|
||||||
@ -34,42 +35,28 @@ interface UIFiltersProps {
|
|||||||
updateFilterValue: (id: number, value: FilterValue['value']) => void;
|
updateFilterValue: (id: number, value: FilterValue['value']) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterWrapper = styled.div`
|
|
||||||
display: inline-block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function UIFilters({
|
function UIFilters({
|
||||||
filters,
|
filters,
|
||||||
internalFilters = [],
|
internalFilters = [],
|
||||||
updateFilterValue,
|
updateFilterValue,
|
||||||
}: UIFiltersProps) {
|
}: UIFiltersProps) {
|
||||||
return (
|
return (
|
||||||
<FilterWrapper>
|
<>
|
||||||
{filters.map(
|
{filters.map(
|
||||||
(
|
({ Header, fetchSelects, id, input, paginate, selects }, index) => {
|
||||||
{
|
|
||||||
Header,
|
|
||||||
fetchSelects,
|
|
||||||
id,
|
|
||||||
input,
|
|
||||||
paginate,
|
|
||||||
selects,
|
|
||||||
unfilteredLabel,
|
|
||||||
},
|
|
||||||
index,
|
|
||||||
) => {
|
|
||||||
const initialValue =
|
const initialValue =
|
||||||
internalFilters[index] && internalFilters[index].value;
|
internalFilters[index] && internalFilters[index].value;
|
||||||
if (input === 'select') {
|
if (input === 'select') {
|
||||||
return (
|
return (
|
||||||
<SelectFilter
|
<SelectFilter
|
||||||
Header={Header}
|
Header={Header}
|
||||||
emptyLabel={unfilteredLabel}
|
|
||||||
fetchSelects={fetchSelects}
|
fetchSelects={fetchSelects}
|
||||||
initialValue={initialValue}
|
initialValue={initialValue}
|
||||||
key={id}
|
key={id}
|
||||||
name={id}
|
name={id}
|
||||||
onSelect={(value: any) => updateFilterValue(index, value)}
|
onSelect={(option: SelectOption | undefined) =>
|
||||||
|
updateFilterValue(index, option)
|
||||||
|
}
|
||||||
paginate={paginate}
|
paginate={paginate}
|
||||||
selects={selects}
|
selects={selects}
|
||||||
/>
|
/>
|
||||||
@ -100,7 +87,7 @@ function UIFilters({
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</FilterWrapper>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,8 +377,8 @@ describe('ListView', () => {
|
|||||||
expect(wrapper.find(ListViewFilters)).toExist();
|
expect(wrapper.find(ListViewFilters)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetched async filter values on mount', () => {
|
it('does not fetch async filter values on mount', () => {
|
||||||
expect(fetchSelectsMock).toHaveBeenCalled();
|
expect(fetchSelectsMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchData on filter', () => {
|
it('calls fetchData on filter', () => {
|
||||||
@ -387,7 +387,7 @@ describe('ListView', () => {
|
|||||||
.find('[data-test="filters-select"]')
|
.find('[data-test="filters-select"]')
|
||||||
.first()
|
.first()
|
||||||
.props()
|
.props()
|
||||||
.onChange({ value: 'bar' });
|
.onChange({ label: 'bar', value: 'bar' });
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -395,13 +395,15 @@ describe('ListView', () => {
|
|||||||
.find('[data-test="filters-search"]')
|
.find('[data-test="filters-search"]')
|
||||||
.first()
|
.first()
|
||||||
.props()
|
.props()
|
||||||
.onChange({ currentTarget: { value: 'something' } });
|
.onChange({
|
||||||
|
currentTarget: { label: 'something', value: 'something' },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
act(() => {
|
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(`
|
expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(`
|
||||||
@ -411,7 +413,10 @@ describe('ListView', () => {
|
|||||||
Object {
|
Object {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
"operator": "eq",
|
"operator": "eq",
|
||||||
"value": "bar",
|
"value": Object {
|
||||||
|
"label": "bar",
|
||||||
|
"value": "bar",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
@ -433,7 +438,10 @@ describe('ListView', () => {
|
|||||||
Object {
|
Object {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
"operator": "eq",
|
"operator": "eq",
|
||||||
"value": "bar",
|
"value": Object {
|
||||||
|
"label": "bar",
|
||||||
|
"value": "bar",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"id": "name",
|
"id": "name",
|
||||||
@ -462,7 +470,7 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper2.find('[data-test="card-sort-select"]').first().props().onChange({
|
wrapper2.find('[aria-label="Sort"]').first().props().onChange({
|
||||||
desc: false,
|
desc: false,
|
||||||
id: 'something',
|
id: 'something',
|
||||||
label: 'Alphabetical',
|
label: 'Alphabetical',
|
||||||
|
@ -50,13 +50,11 @@ const ListViewStyles = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
padding-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
|
||||||
.header-left {
|
& .controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 5;
|
flex-wrap: wrap;
|
||||||
}
|
column-gap: ${({ theme }) => theme.gridUnit * 6}px;
|
||||||
.header-right {
|
row-gap: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +134,7 @@ const bulkSelectColumnConfig = {
|
|||||||
|
|
||||||
const ViewModeContainer = styled.div`
|
const ViewModeContainer = styled.div`
|
||||||
padding-right: ${({ theme }) => theme.gridUnit * 4}px;
|
padding-right: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit * 5 + 1}px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.toggle-button {
|
.toggle-button {
|
||||||
@ -301,10 +300,10 @@ function ListView<T extends object = any>({
|
|||||||
<ListViewStyles>
|
<ListViewStyles>
|
||||||
<div data-test={className} className={`superset-list-view ${className}`}>
|
<div data-test={className} className={`superset-list-view ${className}`}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-left">
|
{cardViewEnabled && (
|
||||||
{cardViewEnabled && (
|
<ViewModeToggle mode={viewMode} setMode={setViewMode} />
|
||||||
<ViewModeToggle mode={viewMode} setMode={setViewMode} />
|
)}
|
||||||
)}
|
<div className="controls">
|
||||||
{filterable && (
|
{filterable && (
|
||||||
<FilterControls
|
<FilterControls
|
||||||
filters={filters}
|
filters={filters}
|
||||||
@ -312,8 +311,6 @@ function ListView<T extends object = any>({
|
|||||||
updateFilterValue={applyFilterValue}
|
updateFilterValue={applyFilterValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="header-right">
|
|
||||||
{viewMode === 'card' && cardSortSelectOptions && (
|
{viewMode === 'card' && cardSortSelectOptions && (
|
||||||
<CardSortSelect
|
<CardSortSelect
|
||||||
initialSort={initialSort}
|
initialSort={initialSort}
|
||||||
|
@ -53,10 +53,10 @@ export interface Filter {
|
|||||||
selects?: SelectOption[];
|
selects?: SelectOption[];
|
||||||
onFilterOpen?: () => void;
|
onFilterOpen?: () => void;
|
||||||
fetchSelects?: (
|
fetchSelects?: (
|
||||||
filterValue?: string,
|
filterValue: string,
|
||||||
pageIndex?: number,
|
page: number,
|
||||||
pageSize?: number,
|
pageSize: number,
|
||||||
) => Promise<SelectOption[]>;
|
) => Promise<{ data: SelectOption[]; totalCount: number }>;
|
||||||
paginate?: boolean;
|
paginate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,15 @@ export interface FilterValue {
|
|||||||
id: string;
|
id: string;
|
||||||
urlDisplay?: string;
|
urlDisplay?: string;
|
||||||
operator?: 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 {
|
export interface FetchDataConfig {
|
||||||
|
@ -55,6 +55,8 @@ const RisonParam: QueryParamConfig<string, any> = {
|
|||||||
: rison.decode(dataStr),
|
: rison.decode(dataStr),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SELECT_WIDTH = 200;
|
||||||
|
|
||||||
export class ListViewError extends Error {
|
export class ListViewError extends Error {
|
||||||
name = 'ListViewError';
|
name = 'ListViewError';
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -56,6 +56,8 @@ interface AlertListProps {
|
|||||||
isReportEnabled: boolean;
|
isReportEnabled: boolean;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const deleteAlerts = makeApi<number[], { message: string }>({
|
const deleteAlerts = makeApi<number[], { message: string }>({
|
||||||
@ -381,7 +383,7 @@ function AlertList({
|
|||||||
createErrorHandler(errMsg =>
|
createErrorHandler(errMsg =>
|
||||||
t('An error occurred while fetching created by values: %s', errMsg),
|
t('An error occurred while fetching created by values: %s', errMsg),
|
||||||
),
|
),
|
||||||
user.userId,
|
user,
|
||||||
),
|
),
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
|
@ -46,6 +46,8 @@ interface AnnotationLayersListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +303,7 @@ function AnnotationLayersList({
|
|||||||
errMsg,
|
errMsg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
user.userId,
|
user,
|
||||||
),
|
),
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
|
@ -77,42 +77,40 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
|
|||||||
setupPlugins();
|
setupPlugins();
|
||||||
const registry = getChartMetadataRegistry();
|
const registry = getChartMetadataRegistry();
|
||||||
|
|
||||||
const createFetchDatasets = (handleError: (err: Response) => void) => async (
|
const createFetchDatasets = async (
|
||||||
filterValue = '',
|
filterValue = '',
|
||||||
pageIndex?: number,
|
page: number,
|
||||||
pageSize?: number,
|
pageSize: number,
|
||||||
) => {
|
) => {
|
||||||
// add filters if filterValue
|
// add filters if filterValue
|
||||||
const filters = filterValue
|
const filters = filterValue
|
||||||
? { filters: [{ col: 'table_name', opr: 'sw', value: filterValue }] }
|
? { filters: [{ col: 'table_name', opr: 'sw', value: filterValue }] }
|
||||||
: {};
|
: {};
|
||||||
try {
|
const queryParams = rison.encode({
|
||||||
const queryParams = rison.encode({
|
columns: ['datasource_name', 'datasource_id'],
|
||||||
columns: ['datasource_name', 'datasource_id'],
|
keys: ['none'],
|
||||||
keys: ['none'],
|
order_column: 'table_name',
|
||||||
order_column: 'table_name',
|
order_direction: 'asc',
|
||||||
order_direction: 'asc',
|
page,
|
||||||
...(pageIndex ? { page: pageIndex } : {}),
|
page_size: pageSize,
|
||||||
...(pageSize ? { page_size: pageSize } : {}),
|
...filters,
|
||||||
...filters,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { json = {} } = await SupersetClient.get({
|
const { json = {} } = await SupersetClient.get({
|
||||||
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const datasets = json?.result?.map(
|
const datasets = json?.result?.map(
|
||||||
({ table_name: tableName, id }: { table_name: string; id: number }) => ({
|
({ table_name: tableName, id }: { table_name: string; id: number }) => ({
|
||||||
label: tableName,
|
label: tableName,
|
||||||
value: id,
|
value: id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return uniqBy<SelectOption>(datasets, 'value');
|
return {
|
||||||
} catch (e) {
|
data: uniqBy<SelectOption>(datasets, 'value'),
|
||||||
handleError(e);
|
totalCount: json?.count,
|
||||||
}
|
};
|
||||||
return [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChartListProps {
|
interface ChartListProps {
|
||||||
@ -120,6 +118,8 @@ interface ChartListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,113 +417,110 @@ function ChartList(props: ChartListProps) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const favoritesFilter: Filter = {
|
const favoritesFilter: Filter = useMemo(
|
||||||
Header: t('Favorite'),
|
() => ({
|
||||||
id: 'id',
|
Header: t('Favorite'),
|
||||||
urlDisplay: 'favorite',
|
id: 'id',
|
||||||
input: 'select',
|
urlDisplay: 'favorite',
|
||||||
operator: FilterOperator.chartIsFav,
|
input: 'select',
|
||||||
unfilteredLabel: t('Any'),
|
operator: FilterOperator.chartIsFav,
|
||||||
selects: [
|
unfilteredLabel: t('Any'),
|
||||||
{ label: t('Yes'), value: true },
|
selects: [
|
||||||
{ label: t('No'), value: false },
|
{ label: t('Yes'), value: true },
|
||||||
],
|
{ label: t('No'), value: false },
|
||||||
};
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const filters: Filters = [
|
const filters: Filters = useMemo(
|
||||||
{
|
() => [
|
||||||
Header: t('Owner'),
|
{
|
||||||
id: 'owners',
|
Header: t('Owner'),
|
||||||
input: 'select',
|
id: 'owners',
|
||||||
operator: FilterOperator.relationManyMany,
|
input: 'select',
|
||||||
unfilteredLabel: t('All'),
|
operator: FilterOperator.relationManyMany,
|
||||||
fetchSelects: createFetchRelated(
|
unfilteredLabel: t('All'),
|
||||||
'chart',
|
fetchSelects: createFetchRelated(
|
||||||
'owners',
|
'chart',
|
||||||
createErrorHandler(errMsg =>
|
'owners',
|
||||||
addDangerToast(
|
createErrorHandler(errMsg =>
|
||||||
t(
|
addDangerToast(
|
||||||
'An error occurred while fetching chart owners values: %s',
|
t(
|
||||||
errMsg,
|
'An error occurred while fetching chart owners values: %s',
|
||||||
|
errMsg,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
props.user,
|
||||||
),
|
),
|
||||||
props.user.userId,
|
paginate: true,
|
||||||
),
|
},
|
||||||
paginate: true,
|
{
|
||||||
},
|
Header: t('Created by'),
|
||||||
{
|
id: 'created_by',
|
||||||
Header: t('Created by'),
|
input: 'select',
|
||||||
id: 'created_by',
|
operator: FilterOperator.relationOneMany,
|
||||||
input: 'select',
|
unfilteredLabel: t('All'),
|
||||||
operator: FilterOperator.relationOneMany,
|
fetchSelects: createFetchRelated(
|
||||||
unfilteredLabel: t('All'),
|
'chart',
|
||||||
fetchSelects: createFetchRelated(
|
'created_by',
|
||||||
'chart',
|
createErrorHandler(errMsg =>
|
||||||
'created_by',
|
addDangerToast(
|
||||||
createErrorHandler(errMsg =>
|
t(
|
||||||
addDangerToast(
|
'An error occurred while fetching chart created by values: %s',
|
||||||
t(
|
errMsg,
|
||||||
'An error occurred while fetching chart created by values: %s',
|
),
|
||||||
errMsg,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
props.user,
|
||||||
),
|
),
|
||||||
props.user.userId,
|
paginate: true,
|
||||||
),
|
},
|
||||||
paginate: true,
|
{
|
||||||
},
|
Header: t('Viz type'),
|
||||||
{
|
id: 'viz_type',
|
||||||
Header: t('Viz type'),
|
input: 'select',
|
||||||
id: 'viz_type',
|
operator: FilterOperator.equals,
|
||||||
input: 'select',
|
unfilteredLabel: t('All'),
|
||||||
operator: FilterOperator.equals,
|
selects: registry
|
||||||
unfilteredLabel: t('All'),
|
.keys()
|
||||||
selects: registry
|
.filter(k => nativeFilterGate(registry.get(k)?.behaviors || []))
|
||||||
.keys()
|
.map(k => ({ label: registry.get(k)?.name || k, value: k }))
|
||||||
.filter(k => nativeFilterGate(registry.get(k)?.behaviors || []))
|
.sort((a, b) => {
|
||||||
.map(k => ({ label: registry.get(k)?.name || k, value: k }))
|
if (!a.label || !b.label) {
|
||||||
.sort((a, b) => {
|
return 0;
|
||||||
if (!a.label || !b.label) {
|
}
|
||||||
|
|
||||||
|
if (a.label > b.label) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.label < b.label) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}),
|
||||||
|
},
|
||||||
if (a.label > b.label) {
|
{
|
||||||
return 1;
|
Header: t('Dataset'),
|
||||||
}
|
id: 'datasource_id',
|
||||||
if (a.label < b.label) {
|
input: 'select',
|
||||||
return -1;
|
operator: FilterOperator.equals,
|
||||||
}
|
unfilteredLabel: t('All'),
|
||||||
|
fetchSelects: createFetchDatasets,
|
||||||
return 0;
|
paginate: true,
|
||||||
}),
|
},
|
||||||
},
|
...(props.user.userId ? [favoritesFilter] : []),
|
||||||
{
|
{
|
||||||
Header: t('Dataset'),
|
Header: t('Search'),
|
||||||
id: 'datasource_id',
|
id: 'slice_name',
|
||||||
input: 'select',
|
input: 'search',
|
||||||
operator: FilterOperator.equals,
|
operator: FilterOperator.chartAllText,
|
||||||
unfilteredLabel: t('All'),
|
},
|
||||||
fetchSelects: createFetchDatasets(
|
],
|
||||||
createErrorHandler(errMsg =>
|
[addDangerToast, favoritesFilter, props.user],
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortTypes = [
|
const sortTypes = [
|
||||||
{
|
{
|
||||||
|
@ -45,6 +45,8 @@ interface CssTemplatesListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +289,7 @@ function CssTemplatesList({
|
|||||||
errMsg,
|
errMsg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
user.userId,
|
user,
|
||||||
),
|
),
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
|
@ -71,6 +71,8 @@ interface DashboardListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,81 +406,87 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const favoritesFilter: Filter = {
|
const favoritesFilter: Filter = useMemo(
|
||||||
Header: t('Favorite'),
|
() => ({
|
||||||
id: 'id',
|
Header: t('Favorite'),
|
||||||
urlDisplay: 'favorite',
|
id: 'id',
|
||||||
input: 'select',
|
urlDisplay: 'favorite',
|
||||||
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',
|
|
||||||
input: 'select',
|
input: 'select',
|
||||||
operator: FilterOperator.relationManyMany,
|
operator: FilterOperator.dashboardIsFav,
|
||||||
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,
|
|
||||||
unfilteredLabel: t('Any'),
|
unfilteredLabel: t('Any'),
|
||||||
selects: [
|
selects: [
|
||||||
{ label: t('Published'), value: true },
|
{ label: t('Yes'), value: true },
|
||||||
{ label: t('Draft'), value: false },
|
{ label: t('No'), value: false },
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
...(props.user.userId ? [favoritesFilter] : []),
|
[],
|
||||||
{
|
);
|
||||||
Header: t('Search'),
|
|
||||||
id: 'dashboard_title',
|
const filters: Filters = useMemo(
|
||||||
input: 'search',
|
() => [
|
||||||
operator: FilterOperator.titleOrSlug,
|
{
|
||||||
},
|
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 = [
|
const sortTypes = [
|
||||||
{
|
{
|
||||||
|
@ -161,13 +161,13 @@ describe('DatabaseList', () => {
|
|||||||
.find('[name="expose_in_sqllab"]')
|
.find('[name="expose_in_sqllab"]')
|
||||||
.first()
|
.first()
|
||||||
.props()
|
.props()
|
||||||
.onSelect(true);
|
.onSelect({ label: 'Yes', value: true });
|
||||||
|
|
||||||
filtersWrapper
|
filtersWrapper
|
||||||
.find('[name="allow_run_async"]')
|
.find('[name="allow_run_async"]')
|
||||||
.first()
|
.first()
|
||||||
.props()
|
.props()
|
||||||
.onSelect(false);
|
.onSelect({ label: 'Yes', value: false });
|
||||||
|
|
||||||
filtersWrapper
|
filtersWrapper
|
||||||
.find('[name="database_name"]')
|
.find('[name="database_name"]')
|
||||||
|
@ -118,12 +118,14 @@ describe('DatasetList', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches owner filter values', () => {
|
it('does not fetch owner filter values on mount', async () => {
|
||||||
expect(fetchMock.calls(/dataset\/related\/owners/)).toHaveLength(1);
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(/dataset\/related\/owners/)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches schema filter values', () => {
|
it('does not fetch schema filter values on mount', async () => {
|
||||||
expect(fetchMock.calls(/dataset\/distinct\/schema/)).toHaveLength(1);
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(/dataset\/distinct\/schema/)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows/hides bulk actions when bulk actions is clicked', async () => {
|
it('shows/hides bulk actions when bulk actions is clicked', async () => {
|
||||||
|
@ -98,6 +98,8 @@ interface DatasetListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +416,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
errMsg,
|
errMsg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
user.userId,
|
user,
|
||||||
),
|
),
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
|
@ -374,7 +374,7 @@ function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) {
|
|||||||
'user',
|
'user',
|
||||||
createErrorHandler(errMsg =>
|
createErrorHandler(errMsg =>
|
||||||
addDangerToast(
|
addDangerToast(
|
||||||
t('An error occurred while fetching database values: %s', errMsg),
|
t('An error occurred while fetching user values: %s', errMsg),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -271,7 +271,7 @@ describe('RTL', () => {
|
|||||||
|
|
||||||
it('renders an export button in the actions bar', async () => {
|
it('renders an export button in the actions bar', async () => {
|
||||||
// Grab Export action button and mock mouse hovering over it
|
// 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);
|
userEvent.hover(exportActionButton);
|
||||||
|
|
||||||
// Wait for the tooltip to pop up
|
// Wait for the tooltip to pop up
|
||||||
|
@ -141,7 +141,10 @@ export function useListViewResource<D extends object = any>(
|
|||||||
.map(({ id, operator: opr, value }) => ({
|
.map(({ id, operator: opr, value }) => ({
|
||||||
col: id,
|
col: id,
|
||||||
opr,
|
opr,
|
||||||
value,
|
value:
|
||||||
|
value && typeof value === 'object' && 'value' in value
|
||||||
|
? value.value
|
||||||
|
: value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const queryParams = rison.encode({
|
const queryParams = rison.encode({
|
||||||
|
@ -37,32 +37,52 @@ const createFetchResourceMethod = (method: string) => (
|
|||||||
resource: string,
|
resource: string,
|
||||||
relation: string,
|
relation: string,
|
||||||
handleError: (error: Response) => void,
|
handleError: (error: Response) => void,
|
||||||
userId?: string | number,
|
user?: { userId: string | number; firstName: string; lastName: string },
|
||||||
) => async (filterValue = '', pageIndex?: number, pageSize?: number) => {
|
) => async (filterValue = '', page: number, pageSize: number) => {
|
||||||
const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`;
|
const resourceEndpoint = `/api/v1/${resource}/${method}/${relation}`;
|
||||||
const options =
|
const queryParams = rison.encode({
|
||||||
userId && pageIndex === 0 ? [{ label: 'me', value: userId }] : [];
|
filter: filterValue,
|
||||||
try {
|
page,
|
||||||
const queryParams = rison.encode({
|
page_size: pageSize,
|
||||||
...(pageIndex ? { page: pageIndex } : {}),
|
});
|
||||||
...(pageSize ? { page_size: pageSize } : {}),
|
const { json = {} } = await SupersetClient.get({
|
||||||
...(filterValue ? { filter: filterValue } : {}),
|
endpoint: `${resourceEndpoint}?q=${queryParams}`,
|
||||||
});
|
});
|
||||||
const { json = {} } = await SupersetClient.get({
|
|
||||||
endpoint: `${resourceEndpoint}?q=${queryParams}`,
|
|
||||||
});
|
|
||||||
const data = json?.result?.map(
|
|
||||||
({ text: label, value }: { text: string; value: any }) => ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return options.concat(data);
|
let fetchedLoggedUser = false;
|
||||||
} catch (e) {
|
const loggedUser = user
|
||||||
handleError(e);
|
? {
|
||||||
|
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;
|
export const PAGE_SIZE = 5;
|
||||||
|
Loading…
Reference in New Issue
Block a user