feat(dashboard): Add description to the native filter (#17025)

* Adding description works

* Add some tests

* Fix tests

* Styled look good

* Tests successful

* Address PR comments

* fix a test
This commit is contained in:
Ajay M 2021-10-27 14:45:31 -04:00 committed by GitHub
parent faf7c74e44
commit 65f1644208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 54 deletions

View File

@ -52,6 +52,7 @@ export const nativeFilters: NativeFiltersState = {
inverseSelection: false, inverseSelection: false,
}, },
type: NativeFilterType.NATIVE_FILTER, type: NativeFilterType.NATIVE_FILTER,
description: '',
}, },
'NATIVE_FILTER-x9QPw0so1': { 'NATIVE_FILTER-x9QPw0so1': {
id: 'NATIVE_FILTER-x9QPw0so1', id: 'NATIVE_FILTER-x9QPw0so1',
@ -81,6 +82,7 @@ export const nativeFilters: NativeFiltersState = {
inverseSelection: false, inverseSelection: false,
}, },
type: NativeFilterType.NATIVE_FILTER, type: NativeFilterType.NATIVE_FILTER,
description: '2 letter code',
}, },
}, },
}; };

View File

@ -68,6 +68,7 @@ export const nativeFiltersInfo: NativeFiltersState = {
isRequired: false, isRequired: false,
}, },
type: NativeFilterType.NATIVE_FILTER, type: NativeFilterType.NATIVE_FILTER,
description: 'test description',
}, },
}, },
}; };

View File

@ -17,27 +17,27 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { fireEvent, render, screen } from 'spec/helpers/testing-library';
import { mockStore } from 'spec/fixtures/mockStore'; import { mockStore } from 'spec/fixtures/mockStore';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters'; import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
import CascadeFilterControl, { CascadeFilterControlProps } from '.'; import CascadeFilterControl, { CascadeFilterControlProps } from '.';
const mockedProps = { const createProps = (defaultsId = nativeFiltersInfo.filters.DefaultsID) => ({
filter: { filter: {
...nativeFiltersInfo.filters.DefaultsID, ...defaultsId,
cascadeChildren: [ cascadeChildren: [
{ {
...nativeFiltersInfo.filters.DefaultsID, ...defaultsId,
name: 'test child filter 1', name: 'test child filter 1',
cascadeChildren: [], cascadeChildren: [],
}, },
{ {
...nativeFiltersInfo.filters.DefaultsID, ...defaultsId,
name: 'test child filter 2', name: 'test child filter 2',
cascadeChildren: [ cascadeChildren: [
{ {
...nativeFiltersInfo.filters.DefaultsID, ...defaultsId,
name: 'test child of a child filter', name: 'test child of a child filter',
cascadeChildren: [], cascadeChildren: [],
}, },
@ -46,7 +46,7 @@ const mockedProps = {
], ],
}, },
onFilterSelectionChange: jest.fn(), onFilterSelectionChange: jest.fn(),
}; });
const setup = (props: CascadeFilterControlProps) => ( const setup = (props: CascadeFilterControlProps) => (
<Provider store={mockStore}> <Provider store={mockStore}>
@ -55,22 +55,41 @@ const setup = (props: CascadeFilterControlProps) => (
); );
test('should render', () => { test('should render', () => {
const { container } = render(setup(mockedProps)); const { container } = render(setup(createProps()));
expect(container).toBeInTheDocument(); expect(container).toBeInTheDocument();
}); });
test('should render the filter name', () => { test('should render the filter name', () => {
render(setup(mockedProps)); render(setup(createProps()));
expect(screen.getByText('test')).toBeInTheDocument(); expect(screen.getByText('test')).toBeInTheDocument();
}); });
test('should render the children filter names', () => { test('should render the children filter names', () => {
render(setup(mockedProps)); render(setup(createProps()));
expect(screen.getByText('test child filter 1')).toBeInTheDocument(); expect(screen.getByText('test child filter 1')).toBeInTheDocument();
expect(screen.getByText('test child filter 2')).toBeInTheDocument(); expect(screen.getByText('test child filter 2')).toBeInTheDocument();
}); });
test('should render the child of a child filter name', () => { test('should render the child of a child filter name', () => {
render(setup(mockedProps)); render(setup(createProps()));
expect(screen.getByText('test child of a child filter')).toBeInTheDocument(); expect(screen.getByText('test child of a child filter')).toBeInTheDocument();
}); });
test('should render tooltip if description is not empty', async () => {
render(setup(createProps()));
expect(screen.getByText('test')).toBeInTheDocument();
const toolTip = screen.getByText('test')?.parentElement?.querySelector('i');
expect(toolTip).not.toBe(null);
fireEvent.mouseOver(toolTip as HTMLElement);
expect(await screen.findByText('test description')).toBeInTheDocument();
});
test('should not render tooltip if description is empty', () => {
render(
setup(
createProps({ ...nativeFiltersInfo.filters.DefaultsID, description: '' }),
),
);
const toolTip = screen.getByText('test')?.parentElement?.querySelector('i');
expect(toolTip).toBe(null);
});

View File

@ -17,11 +17,12 @@
* under the License. * under the License.
*/ */
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { styled } from '@superset-ui/core'; import { styled, SupersetTheme } from '@superset-ui/core';
import { Form, FormItem } from 'src/components/Form'; import { FormItem as StyledFormItem, Form } from 'src/components/Form';
import { Tooltip } from 'src/components/Tooltip';
import { checkIsMissingRequiredValue } from '../utils';
import FilterValue from './FilterValue'; import FilterValue from './FilterValue';
import { FilterProps } from './types'; import { FilterProps } from './types';
import { checkIsMissingRequiredValue } from '../utils';
const StyledIcon = styled.div` const StyledIcon = styled.div`
position: absolute; position: absolute;
@ -50,8 +51,62 @@ const StyledFilterControlContainer = styled(Form)`
width: 100%; width: 100%;
padding-right: ${({ theme }) => theme.gridUnit * 11}px; padding-right: ${({ theme }) => theme.gridUnit * 11}px;
} }
.ant-form-item-tooltip {
margin-bottom: ${({ theme }) => theme.gridUnit}px;
}
`; `;
const FormItem = styled(StyledFormItem)`
.ant-form-item-label {
label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
&::after {
display: none;
}
}
}
`;
const ToolTipContainer = styled.div`
font-size: ${({ theme }) => theme.typography.sizes.m}px;
display: flex;
`;
const RequiredFieldIndicator = () => (
<span
css={(theme: SupersetTheme) => ({
color: theme.colors.error.base,
fontSize: `${theme.typography.sizes.s}px`,
paddingLeft: '1px',
})}
>
*
</span>
);
const DescriptionToolTip = ({ description }: { description: string }) => (
<ToolTipContainer>
<Tooltip
title={description}
placement="right"
overlayInnerStyle={{
display: '-webkit-box',
overflow: 'hidden',
WebkitLineClamp: 20,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
}}
>
<i
className="fa fa-info-circle text-muted"
css={(theme: SupersetTheme) => ({
paddingLeft: `${theme.gridUnit}px`,
cursor: 'pointer',
})}
/>
</Tooltip>
</ToolTipContainer>
);
const FilterControl: React.FC<FilterProps> = ({ const FilterControl: React.FC<FilterProps> = ({
dataMaskSelected, dataMaskSelected,
filter, filter,
@ -68,6 +123,7 @@ const FilterControl: React.FC<FilterProps> = ({
filter, filter,
filter.dataMask?.filterState, filter.dataMask?.filterState,
); );
const isRequired = !!filter.controlValues?.enableEmptyFilter;
const label = useMemo( const label = useMemo(
() => ( () => (
@ -75,10 +131,14 @@ const FilterControl: React.FC<FilterProps> = ({
<StyledFilterControlTitle data-test="filter-control-name"> <StyledFilterControlTitle data-test="filter-control-name">
{name} {name}
</StyledFilterControlTitle> </StyledFilterControlTitle>
{isRequired && <RequiredFieldIndicator />}
{filter.description && filter.description.trim() && (
<DescriptionToolTip description={filter.description} />
)}
<StyledIcon data-test="filter-icon">{icon}</StyledIcon> <StyledIcon data-test="filter-icon">{icon}</StyledIcon>
</StyledFilterControlTitleBox> </StyledFilterControlTitleBox>
), ),
[icon, name], [name, isRequired, filter.description, icon],
); );
return ( return (
@ -101,4 +161,5 @@ const FilterControl: React.FC<FilterProps> = ({
</StyledFilterControlContainer> </StyledFilterControlContainer>
); );
}; };
export default React.memo(FilterControl); export default React.memo(FilterControl);

View File

@ -16,6 +16,11 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import {
ColumnMeta,
InfoTooltipWithTrigger,
Metric,
} from '@superset-ui/chart-controls';
import { import {
AdhocFilter, AdhocFilter,
Behavior, Behavior,
@ -26,15 +31,11 @@ import {
JsonResponse, JsonResponse,
styled, styled,
SupersetApiError, SupersetApiError,
t,
SupersetClient, SupersetClient,
t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import {
ColumnMeta,
InfoTooltipWithTrigger,
Metric,
} from '@superset-ui/chart-controls';
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/lib/form';
import { isEmpty, isEqual } from 'lodash';
import React, { import React, {
forwardRef, forwardRef,
useCallback, useCallback,
@ -44,53 +45,55 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { isEqual, isEmpty } from 'lodash';
import { FormItem } from 'src/components/Form';
import { Input } from 'src/common/components';
import { Select } from 'src/components';
import { cacheWrapper } from 'src/utils/cacheWrapper';
import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl';
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
import Collapse from 'src/components/Collapse';
import { getChartDataRequest } from 'src/chart/chartAction'; import { getChartDataRequest } from 'src/chart/chartAction';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { Input, TextArea } from 'src/common/components';
import { waitForAsyncData } from 'src/middleware/asyncEvent'; import { Select } from 'src/components';
import Tabs from 'src/components/Tabs'; import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
import { Radio } from 'src/components/Radio';
import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert';
import { FormItem } from 'src/components/Form';
import Icons from 'src/components/Icons';
import Loading from 'src/components/Loading';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { Radio } from 'src/components/Radio';
import Tabs from 'src/components/Tabs';
import { Tooltip } from 'src/components/Tooltip';
import { import {
Chart, Chart,
ChartsState, ChartsState,
DatasourcesState, DatasourcesState,
RootState, RootState,
} from 'src/dashboard/types'; } from 'src/dashboard/types';
import Loading from 'src/components/Loading'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
import { ColumnSelect } from './ColumnSelect'; import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl';
import { NativeFiltersForm, FilterRemoval } from '../types'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { waitForAsyncData } from 'src/middleware/asyncEvent';
import { cacheWrapper } from 'src/utils/cacheWrapper';
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
import { import {
FILTER_SUPPORTED_TYPES, Filter,
hasTemporalColumns, NativeFilterType,
setNativeFilterFieldValues, } from 'src/dashboard/components/nativeFilters/types';
useForceUpdate, import { getFormData } from 'src/dashboard/components/nativeFilters/utils';
mostUsedDataset,
} from './utils';
import { useBackendFormUpdate, useDefaultValue } from './state';
import { getFormData } from '../../utils';
import { Filter, NativeFilterType } from '../../types';
import getControlItemsMap from './getControlItemsMap';
import FilterScope from './FilterScope/FilterScope';
import RemovedFilter from './RemovedFilter';
import DefaultValue from './DefaultValue';
import { CollapsibleControl } from './CollapsibleControl';
import { import {
CASCADING_FILTERS, CASCADING_FILTERS,
getFiltersConfigModalTestId, getFiltersConfigModalTestId,
} from '../FiltersConfigModal'; } from '../FiltersConfigModal';
import { FilterRemoval, NativeFiltersForm } from '../types';
import { CollapsibleControl } from './CollapsibleControl';
import { ColumnSelect } from './ColumnSelect';
import DatasetSelect from './DatasetSelect'; import DatasetSelect from './DatasetSelect';
import DefaultValue from './DefaultValue';
import FilterScope from './FilterScope/FilterScope';
import getControlItemsMap from './getControlItemsMap';
import RemovedFilter from './RemovedFilter';
import { useBackendFormUpdate, useDefaultValue } from './state';
import {
FILTER_SUPPORTED_TYPES,
hasTemporalColumns,
mostUsedDataset,
setNativeFilterFieldValues,
useForceUpdate,
} from './utils';
const TabPane = styled(Tabs.TabPane)` const TabPane = styled(Tabs.TabPane)`
padding: ${({ theme }) => theme.gridUnit * 4}px 0px; padding: ${({ theme }) => theme.gridUnit * 4}px 0px;
@ -966,6 +969,13 @@ const FiltersConfigForm = (
{Object.keys(controlItems) {Object.keys(controlItems)
.filter(key => BASIC_CONTROL_ITEMS.includes(key)) .filter(key => BASIC_CONTROL_ITEMS.includes(key))
.map(key => controlItems[key].element)} .map(key => controlItems[key].element)}
<StyledFormItem
name={['filters', filterId, 'description']}
initialValue={filterToEdit?.description}
label={<StyledLabel>{t('Description')}</StyledLabel>}
>
<TextArea />
</StyledFormItem>
</Collapse.Panel> </Collapse.Panel>
{hasAdvancedSection && ( {hasAdvancedSection && (
<Collapse.Panel <Collapse.Panel

View File

@ -64,6 +64,7 @@ const filterMock: Filter = {
targets: [{}], targets: [{}],
controlValues: {}, controlValues: {},
type: NativeFilterType.NATIVE_FILTER, type: NativeFilterType.NATIVE_FILTER,
description: '',
}; };
const createProps: () => ControlItemsProps = () => ({ const createProps: () => ControlItemsProps = () => ({

View File

@ -45,6 +45,7 @@ export interface NativeFiltersFormItem {
time_range?: string; time_range?: string;
granularity_sqla?: string; granularity_sqla?: string;
type: NativeFilterType; type: NativeFilterType;
description: string;
hierarchicalFilter?: boolean; hierarchicalFilter?: boolean;
} }

View File

@ -159,6 +159,7 @@ export const createHandleSave = (
scope: formInputs.scope, scope: formInputs.scope,
sortMetric: formInputs.sortMetric, sortMetric: formInputs.sortMetric,
type: formInputs.type, type: formInputs.type,
description: (formInputs.description || '').trim(),
}; };
}); });

View File

@ -60,6 +60,7 @@ export interface Filter {
tabsInScope?: string[]; tabsInScope?: string[];
chartsInScope?: number[]; chartsInScope?: number[];
type: NativeFilterType; type: NativeFilterType;
description: string;
} }
export type FilterConfiguration = Filter[]; export type FilterConfiguration = Filter[];