feat: Generic X-axis improvements (#22707)
This commit is contained in:
parent
d591cc8082
commit
af73c59b0a
|
|
@ -46,7 +46,7 @@ const ContentHolder = styled.div`
|
|||
`;
|
||||
|
||||
const TitlesContainer = styled.div`
|
||||
width: 270px;
|
||||
min-width: 270px;
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,19 @@ const StyledAsterisk = styled.span`
|
|||
}
|
||||
`;
|
||||
|
||||
const FilterTypeInfo = styled.div`
|
||||
${({ theme }) => `
|
||||
width: 49%;
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
margin:
|
||||
${-theme.gridUnit * 2}px
|
||||
0px
|
||||
${theme.gridUnit * 4}px
|
||||
${theme.gridUnit * 4}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const FilterTabs = {
|
||||
configuration: {
|
||||
key: 'configuration',
|
||||
|
|
@ -795,6 +808,13 @@ const FiltersConfigForm = (
|
|||
/>
|
||||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
{formFilter?.filterType === 'filter_time' && (
|
||||
<FilterTypeInfo>
|
||||
{t(`Dashboard time range filters apply to temporal columns defined in
|
||||
the filter section of each chart. Add temporal columns to the chart
|
||||
filters to have this dashboard filter impact those charts.`)}
|
||||
</FilterTypeInfo>
|
||||
)}
|
||||
{hasDataset && (
|
||||
<StyledRowContainer>
|
||||
{showDataset ? (
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
useTheme,
|
||||
isDefined,
|
||||
JsonValue,
|
||||
NO_TIME_RANGE,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelSectionConfig,
|
||||
|
|
@ -55,6 +56,7 @@ import Collapse from 'src/components/Collapse';
|
|||
import Tabs from 'src/components/Tabs';
|
||||
import { PluginContext } from 'src/components/DynamicPlugins';
|
||||
import Loading from 'src/components/Loading';
|
||||
import Modal from 'src/components/Modal';
|
||||
|
||||
import { usePrevious } from 'src/hooks/usePrevious';
|
||||
import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||
|
|
@ -66,6 +68,9 @@ import ControlRow from './ControlRow';
|
|||
import Control from './Control';
|
||||
import { ExploreAlert } from './ExploreAlert';
|
||||
import { RunQueryButton } from './RunQueryButton';
|
||||
import { Operators } from '../constants';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
export type ControlPanelsContainerProps = {
|
||||
exploreState: ExplorePageState['explore'];
|
||||
|
|
@ -277,6 +282,55 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
string[] | undefined
|
||||
>(state => state.explore.controlsTransferred);
|
||||
|
||||
const defaultTimeFilter = useSelector<ExplorePageState>(
|
||||
state => state.common?.conf?.DEFAULT_TIME_FILTER,
|
||||
);
|
||||
|
||||
const { form_data, actions } = props;
|
||||
const { setControlValue } = actions;
|
||||
const { x_axis, adhoc_filters } = form_data;
|
||||
|
||||
const previousXAxis = usePrevious(x_axis);
|
||||
|
||||
useEffect(() => {
|
||||
if (x_axis && x_axis !== previousXAxis) {
|
||||
const noFilter =
|
||||
!adhoc_filters ||
|
||||
!adhoc_filters.find(
|
||||
filter =>
|
||||
filter.expressionType === 'SIMPLE' &&
|
||||
filter.operator === 'TEMPORAL_RANGE' &&
|
||||
filter.subject === x_axis,
|
||||
);
|
||||
if (noFilter) {
|
||||
confirm({
|
||||
title: t('The X-axis is not on the filters list'),
|
||||
content:
|
||||
t(`The X-axis is not on the filters list which will prevent it from being used in
|
||||
time range filters in dashboards. Would you like to add it to the filters list?`),
|
||||
onOk: () => {
|
||||
setControlValue('adhoc_filters', [
|
||||
...(adhoc_filters || []),
|
||||
{
|
||||
clause: 'WHERE',
|
||||
subject: x_axis,
|
||||
operator: 'TEMPORAL_RANGE',
|
||||
comparator: defaultTimeFilter || NO_TIME_RANGE,
|
||||
expressionType: 'SIMPLE',
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
x_axis,
|
||||
adhoc_filters,
|
||||
setControlValue,
|
||||
defaultTimeFilter,
|
||||
previousXAxis,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let shouldUpdateControls = false;
|
||||
const removeDatasourceWarningFromControl = (
|
||||
|
|
@ -346,15 +400,11 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
} = useMemo(
|
||||
() =>
|
||||
getState(
|
||||
props.form_data.viz_type,
|
||||
form_data.viz_type,
|
||||
props.exploreState.datasource,
|
||||
props.datasource_type,
|
||||
),
|
||||
[
|
||||
props.exploreState.datasource,
|
||||
props.form_data.viz_type,
|
||||
props.datasource_type,
|
||||
],
|
||||
[props.exploreState.datasource, form_data.viz_type, props.datasource_type],
|
||||
);
|
||||
|
||||
const resetTransferredControls = useCallback(() => {
|
||||
|
|
@ -431,6 +481,32 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
? baseDescription(exploreState, controls[name], chart)
|
||||
: baseDescription;
|
||||
|
||||
if (name === 'adhoc_filters') {
|
||||
restProps.confirmDeletion = {
|
||||
triggerCondition: (
|
||||
valueToBeDeleted: Record<string, any>,
|
||||
values: Record<string, any>[],
|
||||
) => {
|
||||
const isTemporalRange = (filter: Record<string, any>) =>
|
||||
filter.operator === Operators.TEMPORAL_RANGE;
|
||||
if (isTemporalRange(valueToBeDeleted)) {
|
||||
const count = values.filter(isTemporalRange).length;
|
||||
if (count < 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
confirmationTitle: t(
|
||||
'Are you sure you want to remove the last temporal filter?',
|
||||
),
|
||||
confirmationText: t(
|
||||
`This filter is the last temporal filter. If you proceed,
|
||||
this chart won't be affected by time range filters in dashboards.`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Control
|
||||
key={`control-${name}`}
|
||||
|
|
@ -447,7 +523,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
|
||||
const sectionHasHadNoErrors = useResetOnChangeRef(
|
||||
() => ({}),
|
||||
props.form_data.viz_type,
|
||||
form_data.viz_type,
|
||||
);
|
||||
|
||||
const renderControlPanelSection = (
|
||||
|
|
@ -615,7 +691,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
|
||||
const dataTabHasHadNoErrors = useResetOnChangeRef(
|
||||
() => false,
|
||||
props.form_data.viz_type,
|
||||
form_data.viz_type,
|
||||
);
|
||||
|
||||
const dataTabTitle = useMemo(() => {
|
||||
|
|
@ -661,10 +737,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||
]);
|
||||
|
||||
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||
if (
|
||||
!controlPanelRegistry.has(props.form_data.viz_type) &&
|
||||
pluginContext.loading
|
||||
) {
|
||||
if (!controlPanelRegistry.has(form_data.viz_type) && pluginContext.loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
isTemporalColumn,
|
||||
withDndFallback,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import Modal from 'src/components/Modal';
|
||||
import {
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
Operators,
|
||||
|
|
@ -58,6 +59,8 @@ import AdhocFilterControl from '../FilterControl/AdhocFilterControl';
|
|||
import DndAdhocFilterOption from './DndAdhocFilterOption';
|
||||
import { useDefaultTimeFilter } from '../DateFilterControl/utils';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
const DND_ACCEPTED_TYPES = [
|
||||
DndItemType.Column,
|
||||
|
|
@ -75,10 +78,23 @@ export interface DndFilterSelectProps
|
|||
savedMetrics: Metric[];
|
||||
selectedMetrics: QueryFormMetric[];
|
||||
datasource: Datasource;
|
||||
confirmDeletion?: {
|
||||
triggerCondition: (
|
||||
valueToBeDeleted: OptionValueType,
|
||||
values: OptionValueType[],
|
||||
) => boolean;
|
||||
confirmationTitle: string;
|
||||
confirmationText: string;
|
||||
};
|
||||
}
|
||||
|
||||
const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
const { datasource, onChange = () => {}, name: controlName } = props;
|
||||
const {
|
||||
datasource,
|
||||
onChange = () => {},
|
||||
name: controlName,
|
||||
confirmDeletion,
|
||||
} = props;
|
||||
|
||||
const propsValues = Array.from(props.value ?? []);
|
||||
const [values, setValues] = useState(
|
||||
|
|
@ -189,7 +205,7 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
|
|||
);
|
||||
}, [props.value]);
|
||||
|
||||
const onClickClose = useCallback(
|
||||
const removeValue = useCallback(
|
||||
(index: number) => {
|
||||
const valuesCopy = [...values];
|
||||
valuesCopy.splice(index, 1);
|
||||
|
|
@ -199,6 +215,27 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
|
|||
[onChange, values],
|
||||
);
|
||||
|
||||
const onClickClose = useCallback(
|
||||
(index: number) => {
|
||||
if (confirmDeletion) {
|
||||
const { confirmationText, confirmationTitle, triggerCondition } =
|
||||
confirmDeletion;
|
||||
if (triggerCondition(values[index], values)) {
|
||||
confirm({
|
||||
title: confirmationTitle,
|
||||
content: confirmationText,
|
||||
onOk() {
|
||||
removeValue(index);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
removeValue(index);
|
||||
},
|
||||
[confirmDeletion, removeValue, values],
|
||||
);
|
||||
|
||||
const onShiftOptions = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
const newValues = [...values];
|
||||
|
|
@ -404,6 +441,7 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
|
|||
visible={newFilterPopoverVisible}
|
||||
togglePopover={togglePopover}
|
||||
closePopover={closePopover}
|
||||
requireSave={!!droppedItem}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
LabelsContainer,
|
||||
} from 'src/explore/components/controls/OptionControls';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Modal from 'src/components/Modal';
|
||||
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
|
||||
import AdhocFilterOption from 'src/explore/components/controls/FilterControl/AdhocFilterOption';
|
||||
import AdhocFilter, {
|
||||
|
|
@ -51,6 +52,8 @@ import AdhocFilter, {
|
|||
import adhocFilterType from 'src/explore/components/controls/FilterControl/adhocFilterType';
|
||||
import columnType from 'src/explore/components/controls/FilterControl/columnType';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
const selectedMetricType = PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
adhocMetricType,
|
||||
|
|
@ -71,6 +74,11 @@ const propTypes = {
|
|||
PropTypes.arrayOf(selectedMetricType),
|
||||
]),
|
||||
isLoading: PropTypes.bool,
|
||||
confirmDeletion: PropTypes.shape({
|
||||
triggerCondition: PropTypes.func,
|
||||
confirmationTitle: PropTypes.string,
|
||||
confirmationText: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
|
@ -96,6 +104,7 @@ class AdhocFilterControl extends React.Component {
|
|||
this.onChange = this.onChange.bind(this);
|
||||
this.mapOption = this.mapOption.bind(this);
|
||||
this.getMetricExpression = this.getMetricExpression.bind(this);
|
||||
this.removeFilter = this.removeFilter.bind(this);
|
||||
|
||||
const filters = (this.props.value || []).map(filter =>
|
||||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
|
|
@ -173,7 +182,7 @@ class AdhocFilterControl extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onRemoveFilter(index) {
|
||||
removeFilter(index) {
|
||||
const valuesCopy = [...this.state.values];
|
||||
valuesCopy.splice(index, 1);
|
||||
this.setState(prevState => ({
|
||||
|
|
@ -183,6 +192,26 @@ class AdhocFilterControl extends React.Component {
|
|||
this.props.onChange(valuesCopy);
|
||||
}
|
||||
|
||||
onRemoveFilter(index) {
|
||||
const { confirmDeletion } = this.props;
|
||||
const { values } = this.state;
|
||||
if (confirmDeletion) {
|
||||
const { confirmationText, confirmationTitle, triggerCondition } =
|
||||
confirmDeletion;
|
||||
if (triggerCondition(values[index], values)) {
|
||||
confirm({
|
||||
title: confirmationTitle,
|
||||
content: confirmationText,
|
||||
onOk() {
|
||||
this.removeFilter(index);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.removeFilter(index);
|
||||
}
|
||||
|
||||
onNewFilter(newFilter) {
|
||||
const mappedOption = this.mapOption(newFilter);
|
||||
if (mappedOption) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const propTypes = {
|
|||
theme: PropTypes.object,
|
||||
sections: PropTypes.arrayOf(PropTypes.string),
|
||||
operators: PropTypes.arrayOf(PropTypes.string),
|
||||
requireSave: PropTypes.bool,
|
||||
};
|
||||
|
||||
const ResizeIcon = styled.i`
|
||||
|
|
@ -181,12 +182,14 @@ export default class AdhocFilterEditPopover extends React.Component {
|
|||
partitionColumn,
|
||||
theme,
|
||||
operators,
|
||||
requireSave,
|
||||
...popoverProps
|
||||
} = this.props;
|
||||
|
||||
const { adhocFilter } = this.state;
|
||||
const stateIsValid = adhocFilter.isValid();
|
||||
const hasUnsavedChanges = !adhocFilter.equals(propsAdhocFilter);
|
||||
const hasUnsavedChanges =
|
||||
requireSave || !adhocFilter.equals(propsAdhocFilter);
|
||||
|
||||
return (
|
||||
<FilterPopoverContentContainer
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ interface AdhocFilterPopoverTriggerProps {
|
|||
visible?: boolean;
|
||||
togglePopover?: (visible: boolean) => void;
|
||||
closePopover?: () => void;
|
||||
requireSave?: boolean;
|
||||
}
|
||||
|
||||
interface AdhocFilterPopoverTriggerState {
|
||||
|
|
@ -96,6 +97,7 @@ class AdhocFilterPopoverTrigger extends React.PureComponent<
|
|||
sections={this.props.sections}
|
||||
operators={this.props.operators}
|
||||
onChange={this.props.onFilterEdit}
|
||||
requireSave={this.props.requireSave}
|
||||
/>
|
||||
</ExplorePopoverContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -308,7 +308,10 @@ export const OptionControlLabel = ({
|
|||
<CloseContainer
|
||||
role="button"
|
||||
data-test="remove-control-button"
|
||||
onClick={onRemove}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
>
|
||||
<Icons.XSmall iconColor={theme.colors.grayscale.light1} />
|
||||
</CloseContainer>
|
||||
|
|
|
|||
Loading…
Reference in New Issue