feat: Generic X-axis improvements (#22707)

This commit is contained in:
Michael S. Molina 2023-01-19 12:22:39 -05:00 committed by GitHub
parent d591cc8082
commit af73c59b0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 18 deletions

View File

@ -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};
`;

View File

@ -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 ? (

View File

@ -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 />;
}

View File

@ -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}
/>
</>
);

View File

@ -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) {

View File

@ -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

View File

@ -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>
);

View File

@ -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>