diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 76cdea3b3..748d37b13 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -58,6 +58,7 @@ "d3-color": "^1.2.0", "d3-scale": "^2.1.2", "dom-to-image": "^2.6.0", + "emotion-rgba": "0.0.9", "fontsource-fira-code": "^3.0.5", "fontsource-inter": "^3.0.5", "geolib": "^2.0.24", @@ -25709,6 +25710,11 @@ "node": ">= 0.10" } }, + "node_modules/emotion-rgba": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.9.tgz", + "integrity": "sha512-fSt51Lh4a1fppXY3nQrMUC00p1jIYMSaRRkUhPiOJ3s9oumae1tY41AJytRK9d4YmJDP9njJBndgdDn9j7CbsA==" + }, "node_modules/emotion-theming": { "version": "10.0.27", "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.0.27.tgz", @@ -76525,6 +76531,11 @@ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", "dev": true }, + "emotion-rgba": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.9.tgz", + "integrity": "sha512-fSt51Lh4a1fppXY3nQrMUC00p1jIYMSaRRkUhPiOJ3s9oumae1tY41AJytRK9d4YmJDP9njJBndgdDn9j7CbsA==" + }, "emotion-theming": { "version": "10.0.27", "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.0.27.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 772433968..ca806803d 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -110,6 +110,7 @@ "d3-color": "^1.2.0", "d3-scale": "^2.1.2", "dom-to-image": "^2.6.0", + "emotion-rgba": "0.0.9", "fontsource-fira-code": "^3.0.5", "fontsource-inter": "^3.0.5", "geolib": "^2.0.24", diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 8c7485239..20b18d453 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -193,7 +193,7 @@ const FilterValue: React.FC = ({ ) : ( = ({ filterState={{ ...filter.dataMask?.filterState, validateMessage: isMissingRequiredValue && t('Value is required'), - validateStatus: isMissingRequiredValue && 'error', }} ownState={filter.dataMask?.ownState} enableNoResults={metadata?.enableNoResults} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx index d4b0ed84c..85d55807b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx @@ -22,6 +22,7 @@ import { SetDataMaskHook, SuperChart, AppSection, + t, } from '@superset-ui/core'; import { FormInstance } from 'antd/lib/form'; import Loading from 'src/components/Loading'; @@ -56,7 +57,10 @@ const DefaultValue: FC = ({ setLoading(true); } }, [hasDataset, queriesData]); - + const value = formFilter.defaultDataMask?.filterState.value; + const isMissingRequiredValue = + (value === null || value === undefined) && + formFilter?.controlValues?.enableEmptyFilter; return loading ? ( ) : ( @@ -73,7 +77,10 @@ const DefaultValue: FC = ({ chartType={formFilter?.filterType} hooks={{ setDataMask }} enableNoResults={enableNoResults} - filterState={formFilter.defaultDataMask?.filterState} + filterState={{ + ...formFilter.defaultDataMask?.filterState, + validateMessage: isMissingRequiredValue && t('Value is required'), + }} /> ); }; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 32d020457..449b5949a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -749,9 +749,7 @@ const FiltersConfigForm = ( if (hasValue) { return Promise.resolve(); } - return Promise.reject( - new Error(t('Default value is required')), - ); + return Promise.reject(); }, }, ]} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index 80729cb46..caba35a7d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -191,13 +191,13 @@ test('renders a numerical range filter type', () => { expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument(); expect(screen.getByText(DATASET_REGEX)).toBeInTheDocument(); expect(screen.getByText(COLUMN_REGEX)).toBeInTheDocument(); + expect(screen.getByText(REQUIRED_REGEX)).toBeInTheDocument(); expect(getCheckbox(DEFAULT_VALUE_REGEX)).not.toBeChecked(); expect(getCheckbox(APPLY_INSTANTLY_REGEX)).not.toBeChecked(); expect(getCheckbox(PRE_FILTER_REGEX)).not.toBeChecked(); expect(queryCheckbox(MULTIPLE_REGEX)).not.toBeInTheDocument(); - expect(queryCheckbox(REQUIRED_REGEX)).not.toBeInTheDocument(); expect(queryCheckbox(HIERARCHICAL_REGEX)).not.toBeInTheDocument(); expect(queryCheckbox(FIRST_ITEM_REGEX)).not.toBeInTheDocument(); expect(queryCheckbox(INVERSE_SELECTION_REGEX)).not.toBeInTheDocument(); diff --git a/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx b/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx index ce67cdc72..6e8a3bfb5 100644 --- a/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx @@ -16,14 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { ensureIsArray, ExtraFormData, t, tn } from '@superset-ui/core'; +import { ensureIsArray, ExtraFormData, styled, t, tn } from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; import { Select } from 'src/common/components'; import { Styles, StyledSelect } from '../common'; import { PluginFilterGroupByProps } from './types'; +import FormItem from '../../../components/Form/FormItem'; const { Option } = Select; +const Error = styled.div` + color: ${({ theme }) => theme.colors.error.base}; +`; + export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) { const { data, @@ -70,28 +75,36 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) { : tn('%s option', '%s options', columns.length, columns.length); return ( - {filterState.validateMessage}} > - {columns.map( - (row: { column_name: string; verbose_name: string | null }) => { - const { column_name: columnName, verbose_name: verboseName } = row; - return ( - - ); - }, - )} - + + {columns.map( + (row: { column_name: string; verbose_name: string | null }) => { + const { + column_name: columnName, + verbose_name: verboseName, + } = row; + return ( + + ); + }, + )} + + ); } diff --git a/superset-frontend/src/filters/components/GroupBy/controlPanel.ts b/superset-frontend/src/filters/components/GroupBy/controlPanel.ts index 5b9b898e6..970683ec8 100644 --- a/superset-frontend/src/filters/components/GroupBy/controlPanel.ts +++ b/superset-frontend/src/filters/components/GroupBy/controlPanel.ts @@ -44,6 +44,18 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'enableEmptyFilter', + config: { + type: 'CheckboxControl', + label: t('Required'), + default: false, + renderTrigger: true, + description: t('User must select a value for this filter.'), + }, + }, + ], ], }, ], diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx index 8d88e516b..f510a6275 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx @@ -16,12 +16,61 @@ * specific language governing permissions and limitations * under the License. */ -import { getNumberFormatter, NumberFormats, t } from '@superset-ui/core'; +import { + getNumberFormatter, + NumberFormats, + styled, + t, +} from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; import { Slider } from 'src/common/components'; +import { rgba } from 'emotion-rgba'; import { PluginFilterRangeProps } from './types'; import { Styles } from '../common'; import { getRangeExtraFormData } from '../../utils'; +import FormItem from '../../../components/Form/FormItem'; + +const Error = styled.div` + color: ${({ theme }) => theme.colors.error.base}; +`; + +const Wrapper = styled.div<{ validateStatus?: string }>` + border: 1px solid transparent; + &:focus { + border: 1px solid + ${({ theme, validateStatus }) => + theme.colors[validateStatus ? 'error' : 'primary'].base}; + outline: 0; + box-shadow: 0 0 0 3px + ${({ theme, validateStatus }) => + rgba(theme.colors[validateStatus ? 'error' : 'primary'].base, 0.2)}; + } + & .ant-slider { + & .ant-slider-track { + background-color: ${({ theme, validateStatus }) => + validateStatus && theme.colors.error.light1}; + } + & .ant-slider-handle { + border: ${({ theme, validateStatus }) => + validateStatus && `2px solid ${theme.colors.error.light1}`}; + &:focus { + box-shadow: 0 0 0 3px + ${({ theme, validateStatus }) => + rgba(theme.colors[validateStatus ? 'error' : 'primary'].base, 0.2)}; + } + } + &:hover { + & .ant-slider-track { + background-color: ${({ theme, validateStatus }) => + validateStatus && theme.colors.error.base}; + } + & .ant-slider-handle { + border: ${({ theme, validateStatus }) => + validateStatus && `2px solid ${theme.colors.error.base}`}; + } + } + } +`; export default function RangeFilterPlugin(props: PluginFilterRangeProps) { const { @@ -32,7 +81,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { setDataMask, setFocusedFilter, unsetFocusedFilter, - inputRef, filterState, } = props; const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER); @@ -40,7 +88,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { const [row] = data; // @ts-ignore const { min, max }: { min: number; max: number } = row; - const { groupby, defaultValue } = formData; + const { groupby, defaultValue, inputRef } = formData; const [col = ''] = groupby || []; const [value, setValue] = useState<[number, number]>( defaultValue ?? [min, max], @@ -111,19 +159,31 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (

{t('Chosen non-numeric column')}

) : ( -
- numberFormatter(value)} + {filterState.validateMessage}} + > + -
+ validateStatus={filterState.validateMessage} + onFocus={setFocusedFilter} + onBlur={unsetFocusedFilter} + onMouseEnter={setFocusedFilter} + onMouseLeave={unsetFocusedFilter} + > + numberFormatter(value)} + marks={marks} + /> + + )} ); diff --git a/superset-frontend/src/filters/components/Range/controlPanel.ts b/superset-frontend/src/filters/components/Range/controlPanel.ts index 8f9501d7a..ad2ecfc4c 100644 --- a/superset-frontend/src/filters/components/Range/controlPanel.ts +++ b/superset-frontend/src/filters/components/Range/controlPanel.ts @@ -43,6 +43,24 @@ const config: ControlPanelConfig = { ], ], }, + { + label: t('UI Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'enableEmptyFilter', + config: { + type: 'CheckboxControl', + label: t('Required'), + default: false, + renderTrigger: true, + description: t('User must select a value for this filter.'), + }, + }, + ], + ], + }, ], }; diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index 23fd0f35d..9cf1b2e19 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -280,7 +280,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { return ( {filterState.validateMessage}} > ` + padding: 2px; + & > span { + border: 2px solid transparent; + display: inline-block; + border: ${({ theme, validateStatus }) => + validateStatus && `2px solid ${theme.colors.error.base}`}; + } + &:focus { + & > span { + border: 2px solid + ${({ theme, validateStatus }) => + validateStatus ? theme.colors.error.base : theme.colors.primary.base}; + outline: 0; + box-shadow: 0 0 0 2px + ${({ validateStatus }) => + validateStatus + ? 'rgba(224, 67, 85, 12%)' + : 'rgba(32, 167, 201, 0.2)'}; + } + } `; export default function TimeFilterPlugin(props: PluginFilterTimeProps) { @@ -37,7 +56,9 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) { setFocusedFilter, unsetFocusedFilter, width, + height, filterState, + formData: { inputRef }, } = props; const handleTimeRangeChange = (timeRange?: string): void => { @@ -60,8 +81,13 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) { return ( // @ts-ignore - + diff --git a/superset-frontend/src/filters/components/Time/controlPanel.ts b/superset-frontend/src/filters/components/Time/controlPanel.ts index 1b5f12b9a..466991f79 100644 --- a/superset-frontend/src/filters/components/Time/controlPanel.ts +++ b/superset-frontend/src/filters/components/Time/controlPanel.ts @@ -17,10 +17,30 @@ * under the License. */ import { ControlPanelConfig } from '@superset-ui/chart-controls'; +import { t } from '@superset-ui/core'; const config: ControlPanelConfig = { // For control input types, see: superset-frontend/src/explore/components/controls/index.js - controlPanelSections: [], + controlPanelSections: [ + { + label: t('UI Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'enableEmptyFilter', + config: { + type: 'CheckboxControl', + label: t('Required'), + default: false, + renderTrigger: true, + description: t('User must select a value for this filter.'), + }, + }, + ], + ], + }, + ], }; export default config; diff --git a/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx b/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx index 08456baa8..7ea97edeb 100644 --- a/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx @@ -20,6 +20,7 @@ import { ensureIsArray, ExtraFormData, GenericDataType, + styled, t, tn, } from '@superset-ui/core'; @@ -27,9 +28,14 @@ import React, { useEffect, useState } from 'react'; import { Select } from 'src/common/components'; import { Styles, StyledSelect } from '../common'; import { PluginFilterTimeColumnProps } from './types'; +import FormItem from '../../../components/Form/FormItem'; const { Option } = Select; +const Error = styled.div` + color: ${({ theme }) => theme.colors.error.base}; +`; + export default function PluginFilterTimeColumn( props: PluginFilterTimeColumnProps, ) { @@ -83,27 +89,35 @@ export default function PluginFilterTimeColumn( : tn('%s option', '%s options', timeColumns.length, timeColumns.length); return ( - {filterState.validateMessage}} > - {timeColumns.map( - (row: { column_name: string; verbose_name: string | null }) => { - const { column_name: columnName, verbose_name: verboseName } = row; - return ( - - ); - }, - )} - + + {timeColumns.map( + (row: { column_name: string; verbose_name: string | null }) => { + const { + column_name: columnName, + verbose_name: verboseName, + } = row; + return ( + + ); + }, + )} + + ); } diff --git a/superset-frontend/src/filters/components/TimeColumn/controlPanel.ts b/superset-frontend/src/filters/components/TimeColumn/controlPanel.ts index 5b7bc3dc3..448c0cea6 100644 --- a/superset-frontend/src/filters/components/TimeColumn/controlPanel.ts +++ b/superset-frontend/src/filters/components/TimeColumn/controlPanel.ts @@ -17,9 +17,29 @@ * under the License. */ import { ControlPanelConfig } from '@superset-ui/chart-controls'; +import { t } from '@superset-ui/core'; const config: ControlPanelConfig = { - controlPanelSections: [], + controlPanelSections: [ + { + label: t('UI Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'enableEmptyFilter', + config: { + type: 'CheckboxControl', + label: t('Required'), + default: false, + renderTrigger: true, + description: t('User must select a value for this filter.'), + }, + }, + ], + ], + }, + ], }; export default config; diff --git a/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx b/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx index aabc86786..1cde951a6 100644 --- a/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx @@ -19,6 +19,7 @@ import { ensureIsArray, ExtraFormData, + styled, t, TimeGranularity, tn, @@ -27,9 +28,14 @@ import React, { useEffect, useState } from 'react'; import { Select } from 'src/common/components'; import { Styles, StyledSelect } from '../common'; import { PluginFilterTimeGrainProps } from './types'; +import FormItem from '../../../components/Form/FormItem'; const { Option } = Select; +const Error = styled.div` + color: ${({ theme }) => theme.colors.error.base}; +`; + export default function PluginFilterTimegrain( props: PluginFilterTimeGrainProps, ) { @@ -80,25 +86,30 @@ export default function PluginFilterTimegrain( : tn('%s option', '%s options', data.length, data.length); return ( - {filterState.validateMessage}} > - {(data || []).map((row: { name: string; duration: string }) => { - const { name, duration } = row; - return ( - - ); - })} - + + {(data || []).map((row: { name: string; duration: string }) => { + const { name, duration } = row; + return ( + + ); + })} + + ); } diff --git a/superset-frontend/src/filters/components/TimeGrain/controlPanel.ts b/superset-frontend/src/filters/components/TimeGrain/controlPanel.ts index 5b7bc3dc3..448c0cea6 100644 --- a/superset-frontend/src/filters/components/TimeGrain/controlPanel.ts +++ b/superset-frontend/src/filters/components/TimeGrain/controlPanel.ts @@ -17,9 +17,29 @@ * under the License. */ import { ControlPanelConfig } from '@superset-ui/chart-controls'; +import { t } from '@superset-ui/core'; const config: ControlPanelConfig = { - controlPanelSections: [], + controlPanelSections: [ + { + label: t('UI Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'enableEmptyFilter', + config: { + type: 'CheckboxControl', + label: t('Required'), + default: false, + renderTrigger: true, + description: t('User must select a value for this filter.'), + }, + }, + ], + ], + }, + ], }; export default config;