fix(native-filters): Fix required filters (#15572)

* fix:fix get permission function

* fix: filters required state

* fix: fix CR notes

* fix: removre required message

* fix: fix validation state
This commit is contained in:
simcha90 2021-07-12 14:55:11 +03:00 committed by GitHub
parent 62a8f2e193
commit d70ac21054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 180 additions and 148 deletions

View File

@ -204,7 +204,7 @@ const FilterValue: React.FC<FilterProps> = ({
); );
const filterState = { const filterState = {
...filter.dataMask?.filterState, ...filter.dataMask?.filterState,
validateMessage: isMissingRequiredValue && t('Value is required'), validateStatus: isMissingRequiredValue && 'error',
}; };
if (filterState.value === undefined && preselection) { if (filterState.value === undefined && preselection) {
filterState.value = preselection; filterState.value = preselection;

View File

@ -30,6 +30,7 @@ import { NativeFiltersForm } from '../types';
import { getFormData } from '../../utils'; import { getFormData } from '../../utils';
type DefaultValueProps = { type DefaultValueProps = {
hasDefaultValue: boolean;
filterId: string; filterId: string;
setDataMask: SetDataMaskHook; setDataMask: SetDataMaskHook;
hasDataset: boolean; hasDataset: boolean;
@ -39,6 +40,7 @@ type DefaultValueProps = {
}; };
const DefaultValue: FC<DefaultValueProps> = ({ const DefaultValue: FC<DefaultValueProps> = ({
hasDefaultValue,
filterId, filterId,
hasDataset, hasDataset,
form, form,
@ -59,8 +61,7 @@ const DefaultValue: FC<DefaultValueProps> = ({
}, [hasDataset, queriesData]); }, [hasDataset, queriesData]);
const value = formFilter.defaultDataMask?.filterState.value; const value = formFilter.defaultDataMask?.filterState.value;
const isMissingRequiredValue = const isMissingRequiredValue =
(value === null || value === undefined) && hasDefaultValue && (value === null || value === undefined);
formFilter?.controlValues?.enableEmptyFilter;
return loading ? ( return loading ? (
<Loading position="inline-centered" /> <Loading position="inline-centered" />
) : ( ) : (
@ -80,6 +81,7 @@ const DefaultValue: FC<DefaultValueProps> = ({
filterState={{ filterState={{
...formFilter.defaultDataMask?.filterState, ...formFilter.defaultDataMask?.filterState,
validateMessage: isMissingRequiredValue && t('Value is required'), validateMessage: isMissingRequiredValue && t('Value is required'),
validateStatus: isMissingRequiredValue && 'error',
}} }}
/> />
); );

View File

@ -756,64 +756,69 @@ const FiltersConfigForm = (
checked={hasDefaultValue} checked={hasDefaultValue}
onChange={value => setHasDefaultValue(value)} onChange={value => setHasDefaultValue(value)}
> >
<StyledRowSubFormItem {formFilter.filterType && (
name={['filters', filterId, 'defaultDataMask']} <StyledRowSubFormItem
initialValue={ name={['filters', filterId, 'defaultDataMask']}
formFilter.filterType === filterToEdit?.filterType initialValue={
? filterToEdit?.defaultDataMask formFilter.filterType === filterToEdit?.filterType
: null ? filterToEdit?.defaultDataMask
} : null
data-test="default-input" }
label={<StyledLabel>{t('Default Value')}</StyledLabel>} data-test="default-input"
required={hasDefaultValue} label={<StyledLabel>{t('Default Value')}</StyledLabel>}
rules={[ required={hasDefaultValue}
{ rules={[
validator: (rule, value) => { {
const hasValue = !!value?.filterState?.value; validator: (rule, value) => {
if (hasValue) { const hasValue =
return Promise.resolve(); value?.filterState?.value !== null &&
} value?.filterState?.value !== undefined;
return Promise.reject( if (hasValue) {
new Error(t('Default value is required')), return Promise.resolve();
); }
return Promise.reject(
new Error(t('Default value is required')),
);
},
}, },
}, ]}
]} >
> {error ? (
{error ? ( <BasicErrorAlert
<BasicErrorAlert title={t('Cannot load filter')}
title={t('Cannot load filter')} body={error}
body={error} level="error"
level="error"
/>
) : showDefaultValue ? (
<DefaultValueContainer>
<DefaultValue
setDataMask={dataMask => {
setNativeFilterFieldValues(form, filterId, {
defaultDataMask: dataMask,
});
form.validateFields([
['filters', filterId, 'defaultDataMask'],
]);
forceUpdate();
}}
filterId={filterId}
hasDataset={hasDataset}
form={form}
formData={newFormData}
enableNoResults={enableNoResults}
/> />
{hasDataset && datasetId && ( ) : showDefaultValue ? (
<Tooltip title={t('Refresh the default values')}> <DefaultValueContainer>
<RefreshIcon onClick={() => refreshHandler(true)} /> <DefaultValue
</Tooltip> setDataMask={dataMask => {
)} setNativeFilterFieldValues(form, filterId, {
</DefaultValueContainer> defaultDataMask: dataMask,
) : ( });
t('Fill all required fields to enable "Default Value"') form.validateFields([
)} ['filters', filterId, 'defaultDataMask'],
</StyledRowSubFormItem> ]);
forceUpdate();
}}
hasDefaultValue={hasDefaultValue}
filterId={filterId}
hasDataset={hasDataset}
form={form}
formData={newFormData}
enableNoResults={enableNoResults}
/>
{hasDataset && datasetId && (
<Tooltip title={t('Refresh the default values')}>
<RefreshIcon onClick={() => refreshHandler(true)} />
</Tooltip>
)}
</DefaultValueContainer>
) : (
t('Fill all required fields to enable "Default Value"')
)}
</StyledRowSubFormItem>
)}
</CollapsibleControl> </CollapsibleControl>
{Object.keys(controlItems) {Object.keys(controlItems)
.filter(key => BASIC_CONTROL_ITEMS.includes(key)) .filter(key => BASIC_CONTROL_ITEMS.includes(key))

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { useCallback, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormInstance } from 'antd/lib/form'; import { FormInstance } from 'antd/lib/form';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { NativeFiltersForm, NativeFiltersFormItem } from '../types'; import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
@ -52,27 +52,19 @@ export const useBackendFormUpdate = (
export const useDefaultValue = ( export const useDefaultValue = (
formFilter?: NativeFiltersFormItem, formFilter?: NativeFiltersFormItem,
filterToEdit?: Filter, filterToEdit?: Filter,
) => { ): [boolean, boolean, string, Function] => {
const [hasDefaultValue, setHasPartialDefaultValue] = useState( const enableEmptyFilter = !!formFilter?.controlValues?.enableEmptyFilter;
!!filterToEdit?.defaultDataMask?.filterState?.value, const defaultToFirstItem = !!formFilter?.controlValues?.defaultToFirstItem;
);
const [isRequired, setisRequired] = useState(
formFilter?.controlValues?.enableEmptyFilter,
);
const [hasDefaultValue, setHasPartialDefaultValue] = useState(false);
const [isRequired, setIsRequired] = useState(enableEmptyFilter);
const [defaultValueTooltip, setDefaultValueTooltip] = useState(''); const [defaultValueTooltip, setDefaultValueTooltip] = useState('');
const defaultToFirstItem = formFilter?.controlValues?.defaultToFirstItem; const setHasDefaultValue = (value = false) => {
const required = enableEmptyFilter && !defaultToFirstItem;
const setHasDefaultValue = useCallback( setIsRequired(required);
(value?) => { setHasPartialDefaultValue(required ? true : value);
const required = };
!!formFilter?.controlValues?.enableEmptyFilter && !defaultToFirstItem;
setisRequired(required);
setHasPartialDefaultValue(required ? true : value);
},
[formFilter?.controlValues?.enableEmptyFilter, defaultToFirstItem],
);
useEffect(() => { useEffect(() => {
setHasDefaultValue( setHasDefaultValue(
@ -80,7 +72,16 @@ export const useDefaultValue = (
? false ? false
: !!formFilter?.defaultDataMask?.filterState?.value, : !!formFilter?.defaultDataMask?.filterState?.value,
); );
}, [setHasDefaultValue, defaultToFirstItem]); // TODO: this logic should be unhardcoded
}, [defaultToFirstItem, enableEmptyFilter]);
useEffect(() => {
setHasDefaultValue(
defaultToFirstItem
? false
: !!filterToEdit?.defaultDataMask?.filterState?.value,
);
}, []);
useEffect(() => { useEffect(() => {
let tooltip = ''; let tooltip = '';

View File

@ -164,16 +164,6 @@ export function FiltersConfigModal({
addFilter, addFilter,
); );
// After this, it should be as if the modal was just opened fresh.
// Called when the modal is closed.
const resetForm = () => {
form.resetFields();
setNewFilterIds([]);
setCurrentFilterId(initialCurrentFilterId);
setRemovedFilters({});
setSaveAlertVisible(false);
};
const getFilterTitle = (id: string) => const getFilterTitle = (id: string) =>
formValues.filters[id]?.name ?? formValues.filters[id]?.name ??
filterConfigMap[id]?.name ?? filterConfigMap[id]?.name ??
@ -209,7 +199,6 @@ export function FiltersConfigModal({
filterConfigMap, filterConfigMap,
filterIds, filterIds,
removedFilters, removedFilters,
resetForm,
onSave, onSave,
values, values,
)(); )();
@ -219,7 +208,6 @@ export function FiltersConfigModal({
}; };
const handleConfirmCancel = () => { const handleConfirmCancel = () => {
resetForm();
onCancel(); onCancel();
}; };

View File

@ -104,7 +104,6 @@ export const createHandleSave = (
filterConfigMap: Record<string, Filter>, filterConfigMap: Record<string, Filter>,
filterIds: string[], filterIds: string[],
removedFilters: Record<string, FilterRemoval>, removedFilters: Record<string, FilterRemoval>,
resetForm: Function,
saveForm: Function, saveForm: Function,
values: NativeFiltersForm, values: NativeFiltersForm,
) => async () => { ) => async () => {
@ -145,7 +144,6 @@ export const createHandleSave = (
}); });
await saveForm(newFilterConfig); await saveForm(newFilterConfig);
resetForm();
}; };
export const createHandleTabEdit = ( export const createHandleTabEdit = (

View File

@ -16,18 +16,15 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { ensureIsArray, ExtraFormData, styled, t, tn } from '@superset-ui/core'; import { ensureIsArray, ExtraFormData, t, tn } from '@superset-ui/core';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Select } from 'src/common/components'; import { Select } from 'src/common/components';
import { Styles, StyledSelect, StyledFormItem } from '../common'; import { FormItemProps } from 'antd/lib/form';
import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common';
import { PluginFilterGroupByProps } from './types'; import { PluginFilterGroupByProps } from './types';
const { Option } = Select; const { Option } = Select;
const Error = styled.div`
color: ${({ theme }) => theme.colors.error.base};
`;
export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) { export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
const { const {
data, data,
@ -84,11 +81,20 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
columns.length === 0 columns.length === 0
? t('No columns') ? t('No columns')
: tn('%s option', '%s options', columns.length, columns.length); : tn('%s option', '%s options', columns.length, columns.length);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return ( return (
<Styles height={height} width={width}> <Styles height={height} width={width}>
<StyledFormItem <StyledFormItem
validateStatus={filterState.validateMessage && 'error'} validateStatus={filterState.validateStatus}
extra={<Error>{filterState.validateMessage}</Error>} {...formItemData}
> >
<StyledSelect <StyledSelect
allowClear allowClear

View File

@ -25,47 +25,44 @@ import {
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Slider } from 'src/common/components'; import { Slider } from 'src/common/components';
import { rgba } from 'emotion-rgba'; import { rgba } from 'emotion-rgba';
import { FormItemProps } from 'antd/lib/form';
import { PluginFilterRangeProps } from './types'; import { PluginFilterRangeProps } from './types';
import { StyledFormItem, Styles } from '../common'; import { StatusMessage, StyledFormItem, Styles } from '../common';
import { getRangeExtraFormData } from '../../utils'; import { getRangeExtraFormData } from '../../utils';
const Error = styled.div` const Wrapper = styled.div<{ validateStatus?: 'error' | 'warning' | 'info' }>`
color: ${({ theme }) => theme.colors.error.base};
`;
const Wrapper = styled.div<{ validateStatus?: string }>`
border: 1px solid transparent; border: 1px solid transparent;
&:focus { &:focus {
border: 1px solid border: 1px solid
${({ theme, validateStatus }) => ${({ theme, validateStatus }) =>
theme.colors[validateStatus ? 'error' : 'primary'].base}; theme.colors[validateStatus || 'primary']?.base};
outline: 0; outline: 0;
box-shadow: 0 0 0 3px box-shadow: 0 0 0 3px
${({ theme, validateStatus }) => ${({ theme, validateStatus }) =>
rgba(theme.colors[validateStatus ? 'error' : 'primary'].base, 0.2)}; rgba(theme.colors[validateStatus || 'primary']?.base, 0.2)};
} }
& .ant-slider { & .ant-slider {
& .ant-slider-track { & .ant-slider-track {
background-color: ${({ theme, validateStatus }) => background-color: ${({ theme, validateStatus }) =>
validateStatus && theme.colors.error.light1}; validateStatus && theme.colors[validateStatus]?.light1};
} }
& .ant-slider-handle { & .ant-slider-handle {
border: ${({ theme, validateStatus }) => border: ${({ theme, validateStatus }) =>
validateStatus && `2px solid ${theme.colors.error.light1}`}; validateStatus && `2px solid ${theme.colors[validateStatus]?.light1}`};
&:focus { &:focus {
box-shadow: 0 0 0 3px box-shadow: 0 0 0 3px
${({ theme, validateStatus }) => ${({ theme, validateStatus }) =>
rgba(theme.colors[validateStatus ? 'error' : 'primary'].base, 0.2)}; rgba(theme.colors[validateStatus || 'primary']?.base, 0.2)};
} }
} }
&:hover { &:hover {
& .ant-slider-track { & .ant-slider-track {
background-color: ${({ theme, validateStatus }) => background-color: ${({ theme, validateStatus }) =>
validateStatus && theme.colors.error.base}; validateStatus && theme.colors[validateStatus]?.base};
} }
& .ant-slider-handle { & .ant-slider-handle {
border: ${({ theme, validateStatus }) => border: ${({ theme, validateStatus }) =>
validateStatus && `2px solid ${theme.colors.error.base}`}; validateStatus && `2px solid ${theme.colors[validateStatus]?.base}`};
} }
} }
} }
@ -150,22 +147,31 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
}; };
useEffect(() => { useEffect(() => {
// when switch filter type and queriesData still not updated we need ignore this case (in FilterBar)
if (row?.min === undefined && row?.max === undefined) {
return;
}
handleAfterChange(filterState.value ?? [min, max]); handleAfterChange(filterState.value ?? [min, max]);
}, [JSON.stringify(filterState.value)]); }, [JSON.stringify(filterState.value), JSON.stringify(data)]);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return ( return (
<Styles height={height} width={width}> <Styles height={height} width={width}>
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? ( {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
<h4>{t('Chosen non-numeric column')}</h4> <h4>{t('Chosen non-numeric column')}</h4>
) : ( ) : (
<StyledFormItem <StyledFormItem {...formItemData}>
validateStatus={filterState.validateMessage && 'error'}
extra={<Error>{filterState.validateMessage}</Error>}
>
<Wrapper <Wrapper
tabIndex={-1} tabIndex={-1}
ref={inputRef} ref={inputRef}
validateStatus={filterState.validateMessage} validateStatus={filterState.validateStatus}
onFocus={setFocusedFilter} onFocus={setFocusedFilter}
onBlur={unsetFocusedFilter} onBlur={unsetFocusedFilter}
onMouseEnter={setFocusedFilter} onMouseEnter={setFocusedFilter}

View File

@ -26,7 +26,6 @@ import {
GenericDataType, GenericDataType,
JsonObject, JsonObject,
smartDateDetailedFormatter, smartDateDetailedFormatter,
styled,
t, t,
tn, tn,
} from '@superset-ui/core'; } from '@superset-ui/core';
@ -44,16 +43,13 @@ import { SLOW_DEBOUNCE } from 'src/constants';
import { useImmerReducer } from 'use-immer'; import { useImmerReducer } from 'use-immer';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { usePrevious } from 'src/common/hooks/usePrevious'; import { usePrevious } from 'src/common/hooks/usePrevious';
import { FormItemProps } from 'antd/lib/form';
import { PluginFilterSelectProps, SelectValue } from './types'; import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledFormItem, StyledSelect, Styles } from '../common'; import { StyledFormItem, StyledSelect, Styles, StatusMessage } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils'; import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
const { Option } = Select; const { Option } = Select;
const Error = styled.div`
color: ${({ theme }) => theme.colors.error.base};
`;
type DataMaskAction = type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject } | { type: 'ownState'; ownState: JsonObject }
| { | {
@ -152,6 +148,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
inverseSelection, inverseSelection,
), ),
filterState: { filterState: {
...filterState,
label: values?.length label: values?.length
? `${(values || []).join(', ')}${suffix}` ? `${(values || []).join(', ')}${suffix}`
: undefined, : undefined,
@ -276,11 +273,20 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
: tn('%s option', '%s options', data.length, data.length); : tn('%s option', '%s options', data.length, data.length);
const Icon = inverseSelection ? Icons.StopOutlined : Icons.CheckOutlined; const Icon = inverseSelection ? Icons.StopOutlined : Icons.CheckOutlined;
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return ( return (
<Styles height={height} width={width}> <Styles height={height} width={width}>
<StyledFormItem <StyledFormItem
validateStatus={filterState.validateMessage && 'error'} validateStatus={filterState.validateStatus}
extra={<Error>{filterState.validateMessage}</Error>} {...formItemData}
> >
<StyledSelect <StyledSelect
allowClear allowClear

View File

@ -27,19 +27,23 @@ const TimeFilterStyles = styled(Styles)`
overflow-x: auto; overflow-x: auto;
`; `;
const ControlContainer = styled.div<{ validateStatus?: string }>` const ControlContainer = styled.div<{
validateStatus?: 'error' | 'warning' | 'info';
}>`
padding: 2px; padding: 2px;
& > span { & > span {
border: 2px solid transparent; border: 2px solid transparent;
display: inline-block; display: inline-block;
border: ${({ theme, validateStatus }) => border: ${({ theme, validateStatus }) =>
validateStatus && `2px solid ${theme.colors.error.base}`}; validateStatus && `2px solid ${theme.colors[validateStatus]?.base}`};
} }
&:focus { &:focus {
& > span { & > span {
border: 2px solid border: 2px solid
${({ theme, validateStatus }) => ${({ theme, validateStatus }) =>
validateStatus ? theme.colors.error.base : theme.colors.primary.base}; validateStatus
? theme.colors[validateStatus]?.base
: theme.colors.primary.base};
outline: 0; outline: 0;
box-shadow: 0 0 0 2px box-shadow: 0 0 0 2px
${({ validateStatus }) => ${({ validateStatus }) =>
@ -85,7 +89,7 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
<ControlContainer <ControlContainer
tabIndex={-1} tabIndex={-1}
ref={inputRef} ref={inputRef}
validateStatus={filterState.validateMessage} validateStatus={filterState.validateStatus}
onFocus={setFocusedFilter} onFocus={setFocusedFilter}
onBlur={unsetFocusedFilter} onBlur={unsetFocusedFilter}
onMouseEnter={setFocusedFilter} onMouseEnter={setFocusedFilter}

View File

@ -20,21 +20,17 @@ import {
ensureIsArray, ensureIsArray,
ExtraFormData, ExtraFormData,
GenericDataType, GenericDataType,
styled,
t, t,
tn, tn,
} from '@superset-ui/core'; } from '@superset-ui/core';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Select } from 'src/common/components'; import { Select } from 'src/common/components';
import { Styles, StyledSelect, StyledFormItem } from '../common'; import { FormItemProps } from 'antd/lib/form';
import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common';
import { PluginFilterTimeColumnProps } from './types'; import { PluginFilterTimeColumnProps } from './types';
const { Option } = Select; const { Option } = Select;
const Error = styled.div`
color: ${({ theme }) => theme.colors.error.base};
`;
export default function PluginFilterTimeColumn( export default function PluginFilterTimeColumn(
props: PluginFilterTimeColumnProps, props: PluginFilterTimeColumnProps,
) { ) {
@ -86,11 +82,20 @@ export default function PluginFilterTimeColumn(
timeColumns.length === 0 timeColumns.length === 0
? t('No time columns') ? t('No time columns')
: tn('%s option', '%s options', timeColumns.length, timeColumns.length); : tn('%s option', '%s options', timeColumns.length, timeColumns.length);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return ( return (
<Styles height={height} width={width}> <Styles height={height} width={width}>
<StyledFormItem <StyledFormItem
validateStatus={filterState.validateMessage && 'error'} validateStatus={filterState.validateStatus}
extra={<Error>{filterState.validateMessage}</Error>} {...formItemData}
> >
<StyledSelect <StyledSelect
allowClear allowClear

View File

@ -19,22 +19,18 @@
import { import {
ensureIsArray, ensureIsArray,
ExtraFormData, ExtraFormData,
styled,
t, t,
TimeGranularity, TimeGranularity,
tn, tn,
} from '@superset-ui/core'; } from '@superset-ui/core';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Select } from 'src/common/components'; import { Select } from 'src/common/components';
import { Styles, StyledSelect, StyledFormItem } from '../common'; import { FormItemProps } from 'antd/lib/form';
import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common';
import { PluginFilterTimeGrainProps } from './types'; import { PluginFilterTimeGrainProps } from './types';
const { Option } = Select; const { Option } = Select;
const Error = styled.div`
color: ${({ theme }) => theme.colors.error.base};
`;
export default function PluginFilterTimegrain( export default function PluginFilterTimegrain(
props: PluginFilterTimeGrainProps, props: PluginFilterTimeGrainProps,
) { ) {
@ -96,11 +92,20 @@ export default function PluginFilterTimegrain(
(data || []).length === 0 (data || []).length === 0
? t('No data') ? t('No data')
: tn('%s option', '%s options', data.length, data.length); : tn('%s option', '%s options', data.length, data.length);
const formItemData: FormItemProps = {};
if (filterState.validateMessage) {
formItemData.extra = (
<StatusMessage status={filterState.validateStatus}>
{filterState.validateMessage}
</StatusMessage>
);
}
return ( return (
<Styles height={height} width={width}> <Styles height={height} width={width}>
<StyledFormItem <StyledFormItem
validateStatus={filterState.validateMessage && 'error'} validateStatus={filterState.validateStatus}
extra={<Error>{filterState.validateMessage}</Error>} {...formItemData}
> >
<StyledSelect <StyledSelect
allowClear allowClear

View File

@ -35,3 +35,9 @@ export const StyledFormItem = styled(FormItem)`
margin: 0; margin: 0;
} }
`; `;
export const StatusMessage = styled.div<{
status?: 'error' | 'warning' | 'info';
}>`
color: ${({ theme, status = 'error' }) => theme.colors[status]?.base};
`;