mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
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:
parent
faf7c74e44
commit
65f1644208
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -68,6 +68,7 @@ export const nativeFiltersInfo: NativeFiltersState = {
|
|||||||
isRequired: false,
|
isRequired: false,
|
||||||
},
|
},
|
||||||
type: NativeFilterType.NATIVE_FILTER,
|
type: NativeFilterType.NATIVE_FILTER,
|
||||||
|
description: 'test description',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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 = () => ({
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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[];
|
||||||
|
Loading…
Reference in New Issue
Block a user