mirror of
https://github.com/apache/superset.git
synced 2026-04-22 09:35:23 +00:00
feat: Implement drag and drop columns for filters (#13340)
* Implement DnD feature for filters * minor refactor * Fix types * Fix undefined error * Refactor * Fix ts errors * Fix conflicting dnd types * Bump superset-ui packages * Change DndItemType case to PascalCase * Remove redundant null check * Fix * Fix csrf mock api call
This commit is contained in:
committed by
GitHub
parent
3970d7316b
commit
7b370e6f17
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { ColumnMeta, ColumnOption } from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { LabelProps } from './types';
|
||||
import DndSelectLabel from './DndSelectLabel';
|
||||
import OptionWrapper from './components/OptionWrapper';
|
||||
import { OptionSelector } from './utils';
|
||||
import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
export const DndColumnSelect = (props: LabelProps) => {
|
||||
const { value, options } = props;
|
||||
const optionSelector = new OptionSelector(options, value);
|
||||
const [values, setValues] = useState<ColumnMeta[]>(optionSelector.values);
|
||||
|
||||
const onDrop = (item: DatasourcePanelDndItem) => {
|
||||
const column = item.value as ColumnMeta;
|
||||
if (!optionSelector.isArray && !isEmpty(optionSelector.values)) {
|
||||
optionSelector.replace(0, column.column_name);
|
||||
} else {
|
||||
optionSelector.add(column.column_name);
|
||||
}
|
||||
setValues(optionSelector.values);
|
||||
props.onChange(optionSelector.getValues());
|
||||
};
|
||||
|
||||
const canDrop = (item: DatasourcePanelDndItem) =>
|
||||
!optionSelector.has((item.value as ColumnMeta).column_name);
|
||||
|
||||
const onClickClose = (index: number) => {
|
||||
optionSelector.del(index);
|
||||
setValues(optionSelector.values);
|
||||
props.onChange(optionSelector.getValues());
|
||||
};
|
||||
|
||||
const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
|
||||
optionSelector.swap(dragIndex, hoverIndex);
|
||||
setValues(optionSelector.values);
|
||||
props.onChange(optionSelector.getValues());
|
||||
};
|
||||
|
||||
const valuesRenderer = () =>
|
||||
values.map((column, idx) => (
|
||||
<OptionWrapper
|
||||
key={idx}
|
||||
index={idx}
|
||||
clickClose={onClickClose}
|
||||
onShiftOptions={onShiftOptions}
|
||||
type={DndItemType.ColumnOption}
|
||||
>
|
||||
<ColumnOption column={column} showType />
|
||||
</OptionWrapper>
|
||||
));
|
||||
|
||||
return (
|
||||
<DndSelectLabel<string | string[], ColumnMeta[]>
|
||||
values={values}
|
||||
onDrop={onDrop}
|
||||
canDrop={canDrop}
|
||||
valuesRenderer={valuesRenderer}
|
||||
accept={DndItemType.Column}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import { BaseControlConfig, ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import {
|
||||
AddControlLabel,
|
||||
DndLabelsContainer,
|
||||
HeaderContainer,
|
||||
} from 'src/explore/components/OptionControls';
|
||||
import {
|
||||
DatasourcePanelDndItem,
|
||||
DatasourcePanelDndType,
|
||||
} from 'src/explore/components/DatasourcePanel/types';
|
||||
import Icon from 'src/components/Icon';
|
||||
import OptionWrapper from './components/OptionWrapper';
|
||||
import { OptionSelector } from './utils';
|
||||
|
||||
interface LabelProps extends BaseControlConfig {
|
||||
name: string;
|
||||
value: string[] | string | null;
|
||||
onChange: (value: string[] | string | null) => void;
|
||||
options: { string: ColumnMeta };
|
||||
}
|
||||
|
||||
export default function DndColumnSelectLabel(props: LabelProps) {
|
||||
const theme = useTheme();
|
||||
const { value, options } = props;
|
||||
const optionSelector = new OptionSelector(options, value);
|
||||
const [groupByOptions, setGroupByOptions] = useState<ColumnMeta[]>(
|
||||
optionSelector.groupByOptions,
|
||||
);
|
||||
|
||||
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
|
||||
accept: DatasourcePanelDndType.COLUMN,
|
||||
|
||||
drop: (item: DatasourcePanelDndItem) => {
|
||||
if (!optionSelector.isArray && !isEmpty(optionSelector.groupByOptions)) {
|
||||
optionSelector.replace(0, item.metricOrColumnName);
|
||||
} else {
|
||||
optionSelector.add(item.metricOrColumnName);
|
||||
}
|
||||
setGroupByOptions(optionSelector.groupByOptions);
|
||||
props.onChange(optionSelector.getValues());
|
||||
},
|
||||
|
||||
canDrop: (item: DatasourcePanelDndItem) =>
|
||||
!optionSelector.has(item.metricOrColumnName),
|
||||
|
||||
collect: monitor => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
type: monitor.getItemType(),
|
||||
}),
|
||||
});
|
||||
|
||||
function onClickClose(index: number) {
|
||||
optionSelector.del(index);
|
||||
setGroupByOptions(optionSelector.groupByOptions);
|
||||
props.onChange(optionSelector.getValues());
|
||||
}
|
||||
|
||||
function onShiftOptions(dragIndex: number, hoverIndex: number) {
|
||||
optionSelector.swap(dragIndex, hoverIndex);
|
||||
setGroupByOptions(optionSelector.groupByOptions);
|
||||
props.onChange(optionSelector.getValues());
|
||||
}
|
||||
|
||||
function renderPlaceHolder() {
|
||||
return (
|
||||
<AddControlLabel cancelHover>
|
||||
<Icon name="plus-small" color={theme.colors.grayscale.light1} />
|
||||
{t('Drop Columns')}
|
||||
</AddControlLabel>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOptions() {
|
||||
return groupByOptions.map((column, idx) => (
|
||||
<OptionWrapper
|
||||
key={idx}
|
||||
index={idx}
|
||||
column={column}
|
||||
clickClose={onClickClose}
|
||||
onShiftOptions={onShiftOptions}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={datasourcePanelDrop}>
|
||||
<HeaderContainer>
|
||||
<ControlHeader {...props} />
|
||||
</HeaderContainer>
|
||||
<DndLabelsContainer canDrop={canDrop} isOver={isOver}>
|
||||
{isEmpty(groupByOptions) ? renderPlaceHolder() : renderOptions()}
|
||||
</DndLabelsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with work for additional information
|
||||
* regarding copyright ownership. The ASF licenses file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { logging, SupersetClient } from '@superset-ui/core';
|
||||
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import { OPERATORS } from 'src/explore/constants';
|
||||
import { OptionSortType } from 'src/explore/types';
|
||||
import { DndFilterSelectProps, FilterOptionValueType } from './types';
|
||||
import AdhocFilterPopoverTrigger from '../FilterControl/AdhocFilterPopoverTrigger';
|
||||
import OptionWrapper from './components/OptionWrapper';
|
||||
import DndSelectLabel from './DndSelectLabel';
|
||||
import AdhocFilter, {
|
||||
CLAUSES,
|
||||
EXPRESSION_TYPES,
|
||||
} from '../FilterControl/AdhocFilter';
|
||||
import AdhocMetric from '../MetricControl/AdhocMetric';
|
||||
import {
|
||||
DatasourcePanelDndItem,
|
||||
DndItemValue,
|
||||
} from '../../DatasourcePanel/types';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
const isDictionaryForAdhocFilter = (value: FilterOptionValueType) =>
|
||||
!(value instanceof AdhocFilter) && value?.expressionType;
|
||||
|
||||
export const DndFilterSelect = (props: DndFilterSelectProps) => {
|
||||
const propsValues = Array.from(props.value ?? []);
|
||||
const [values, setValues] = useState(
|
||||
propsValues.map((filter: FilterOptionValueType) =>
|
||||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
),
|
||||
);
|
||||
const [partitionColumn, setPartitionColumn] = useState(undefined);
|
||||
const [newFilterPopoverVisible, setNewFilterPopoverVisible] = useState(false);
|
||||
const [droppedItem, setDroppedItem] = useState<DndItemValue | null>(null);
|
||||
|
||||
const optionsForSelect = (
|
||||
columns: ColumnMeta[],
|
||||
formData: Record<string, any>,
|
||||
) => {
|
||||
const options: OptionSortType[] = [
|
||||
...columns,
|
||||
...[...(formData?.metrics || []), formData?.metric].map(
|
||||
metric =>
|
||||
metric &&
|
||||
(typeof metric === 'string'
|
||||
? { saved_metric_name: metric }
|
||||
: new AdhocMetric(metric)),
|
||||
),
|
||||
].filter(option => option);
|
||||
|
||||
return options
|
||||
.reduce(
|
||||
(
|
||||
results: (OptionSortType & { filterOptionName: string })[],
|
||||
option,
|
||||
) => {
|
||||
if ('saved_metric_name' in option && option.saved_metric_name) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: option.saved_metric_name,
|
||||
});
|
||||
} else if ('column_name' in option && option.column_name) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: `_col_${option.column_name}`,
|
||||
});
|
||||
} else if (option instanceof AdhocMetric) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: `_adhocmetric_${option.label}`,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
},
|
||||
[],
|
||||
)
|
||||
.sort(
|
||||
(a: OptionSortType, b: OptionSortType) =>
|
||||
(a.saved_metric_name || a.column_name || a.label)?.localeCompare(
|
||||
b.saved_metric_name || b.column_name || b.label || '',
|
||||
) ?? 0,
|
||||
);
|
||||
};
|
||||
const [options, setOptions] = useState(
|
||||
optionsForSelect(props.columns, props.formData),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { datasource } = props;
|
||||
if (datasource && datasource.type === 'table') {
|
||||
const dbId = datasource.database?.id;
|
||||
const {
|
||||
datasource_name: name,
|
||||
schema,
|
||||
is_sqllab_view: isSqllabView,
|
||||
} = datasource;
|
||||
|
||||
if (!isSqllabView && dbId && name && schema) {
|
||||
SupersetClient.get({
|
||||
endpoint: `/superset/extra_table_metadata/${dbId}/${name}/${schema}/`,
|
||||
})
|
||||
.then(({ json }: { json: Record<string, any> }) => {
|
||||
if (json && json.partitions) {
|
||||
const { partitions } = json;
|
||||
// for now only show latest_partition option
|
||||
// when table datasource has only 1 partition key.
|
||||
if (
|
||||
partitions &&
|
||||
partitions.cols &&
|
||||
Object.keys(partitions.cols).length === 1
|
||||
) {
|
||||
setPartitionColumn(partitions.cols[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: Record<string, any>) => {
|
||||
logging.error('fetch extra_table_metadata:', error.statusText);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(optionsForSelect(props.columns, props.formData));
|
||||
}, [props.columns, props.formData]);
|
||||
|
||||
useEffect(() => {
|
||||
setValues(
|
||||
(props.value || []).map((filter: FilterOptionValueType) =>
|
||||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
),
|
||||
);
|
||||
}, [props.value]);
|
||||
|
||||
const onClickClose = (index: number) => {
|
||||
const valuesCopy = [...values];
|
||||
valuesCopy.splice(index, 1);
|
||||
setValues(valuesCopy);
|
||||
props.onChange(valuesCopy);
|
||||
};
|
||||
|
||||
const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
|
||||
const newValues = [...values];
|
||||
[newValues[hoverIndex], newValues[dragIndex]] = [
|
||||
newValues[dragIndex],
|
||||
newValues[hoverIndex],
|
||||
];
|
||||
setValues(newValues);
|
||||
};
|
||||
|
||||
const getMetricExpression = (savedMetricName: string) =>
|
||||
props.savedMetrics.find(
|
||||
(savedMetric: Metric) => savedMetric.metric_name === savedMetricName,
|
||||
)?.expression;
|
||||
|
||||
const mapOption = (option: FilterOptionValueType) => {
|
||||
// already a AdhocFilter, skip
|
||||
if (option instanceof AdhocFilter) {
|
||||
return option;
|
||||
}
|
||||
const filterOptions = option as Record<string, any>;
|
||||
// via datasource saved metric
|
||||
if (filterOptions.saved_metric_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
props.datasource.type === 'druid'
|
||||
? filterOptions.saved_metric_name
|
||||
: getMetricExpression(filterOptions.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
}
|
||||
// has a custom label, meaning it's custom column
|
||||
if (filterOptions.label) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
props.datasource.type === 'druid'
|
||||
? filterOptions.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
}
|
||||
// add a new filter item
|
||||
if (filterOptions.column_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: filterOptions.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const onFilterEdit = (changedFilter: AdhocFilter) => {
|
||||
props.onChange(
|
||||
values.map((value: AdhocFilter) => {
|
||||
if (value.filterOptionName === changedFilter.filterOptionName) {
|
||||
return changedFilter;
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onNewFilter = (newFilter: AdhocFilter) => {
|
||||
const mappedOption = mapOption(newFilter);
|
||||
if (mappedOption) {
|
||||
const newValues = [...values, mappedOption];
|
||||
setValues(newValues);
|
||||
props.onChange(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePopover = (visible: boolean) => {
|
||||
setNewFilterPopoverVisible(visible);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const valuesRenderer = () =>
|
||||
values.map((adhocFilter: AdhocFilter, index: number) => {
|
||||
const label = adhocFilter.getDefaultLabel();
|
||||
return (
|
||||
<AdhocFilterPopoverTrigger
|
||||
key={index}
|
||||
adhocFilter={adhocFilter}
|
||||
options={options}
|
||||
datasource={props.datasource}
|
||||
onFilterEdit={onFilterEdit}
|
||||
partitionColumn={partitionColumn}
|
||||
>
|
||||
<OptionWrapper
|
||||
key={index}
|
||||
index={index}
|
||||
clickClose={onClickClose}
|
||||
onShiftOptions={onShiftOptions}
|
||||
type={DndItemType.FilterOption}
|
||||
withCaret
|
||||
>
|
||||
<Tooltip title={label}>{label}</Tooltip>
|
||||
</OptionWrapper>
|
||||
</AdhocFilterPopoverTrigger>
|
||||
);
|
||||
});
|
||||
|
||||
const adhocFilter = useMemo(() => {
|
||||
if (droppedItem?.metric_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
clause: CLAUSES.HAVING,
|
||||
sqlExpression: droppedItem?.expression,
|
||||
});
|
||||
}
|
||||
if (droppedItem instanceof AdhocMetric) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SQL,
|
||||
clause: CLAUSES.HAVING,
|
||||
sqlExpression: (droppedItem as AdhocMetric)?.translateToSql(),
|
||||
});
|
||||
}
|
||||
return new AdhocFilter({
|
||||
subject: (droppedItem as ColumnMeta)?.column_name,
|
||||
});
|
||||
}, [droppedItem]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndSelectLabel<FilterOptionValueType, FilterOptionValueType[]>
|
||||
values={values}
|
||||
onDrop={(item: DatasourcePanelDndItem) => {
|
||||
setDroppedItem(item.value);
|
||||
togglePopover(true);
|
||||
}}
|
||||
canDrop={() => true}
|
||||
valuesRenderer={valuesRenderer}
|
||||
accept={[
|
||||
DndItemType.Column,
|
||||
DndItemType.Metric,
|
||||
DndItemType.MetricOption,
|
||||
DndItemType.AdhocMetricOption,
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
<AdhocFilterPopoverTrigger
|
||||
adhocFilter={adhocFilter}
|
||||
options={options}
|
||||
datasource={props.datasource}
|
||||
onFilterEdit={onNewFilter}
|
||||
partitionColumn={partitionColumn}
|
||||
isControlledComponent
|
||||
visible={newFilterPopoverVisible}
|
||||
togglePopover={togglePopover}
|
||||
closePopover={closePopover}
|
||||
createNew
|
||||
>
|
||||
<div />
|
||||
</AdhocFilterPopoverTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import {
|
||||
AddControlLabel,
|
||||
DndLabelsContainer,
|
||||
HeaderContainer,
|
||||
} from 'src/explore/components/OptionControls';
|
||||
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { DndColumnSelectProps } from './types';
|
||||
|
||||
export default function DndSelectLabel<T, O>(
|
||||
props: DndColumnSelectProps<T, O>,
|
||||
) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
|
||||
accept: props.accept,
|
||||
|
||||
drop: (item: DatasourcePanelDndItem) => {
|
||||
props.onDrop(item);
|
||||
},
|
||||
|
||||
canDrop: (item: DatasourcePanelDndItem) => props.canDrop(item),
|
||||
|
||||
collect: monitor => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
type: monitor.getItemType(),
|
||||
}),
|
||||
});
|
||||
|
||||
function renderPlaceHolder() {
|
||||
return (
|
||||
<AddControlLabel cancelHover>
|
||||
<Icon name="plus-small" color={theme.colors.grayscale.light1} />
|
||||
{t('Drop Columns')}
|
||||
</AddControlLabel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={datasourcePanelDrop}>
|
||||
<HeaderContainer>
|
||||
<ControlHeader {...props} />
|
||||
</HeaderContainer>
|
||||
<DndLabelsContainer canDrop={canDrop} isOver={isOver}>
|
||||
{isEmpty(props.values) ? renderPlaceHolder() : props.valuesRenderer()}
|
||||
</DndLabelsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useTheme } from '@superset-ui/core';
|
||||
import { ColumnOption } from '@superset-ui/chart-controls';
|
||||
import Icon from 'src/components/Icon';
|
||||
import {
|
||||
CaretContainer,
|
||||
@@ -32,7 +31,10 @@ export default function Option(props: OptionProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<OptionControlContainer data-test="option-label">
|
||||
<OptionControlContainer
|
||||
data-test="option-label"
|
||||
withCaret={props.withCaret}
|
||||
>
|
||||
<CloseContainer
|
||||
role="button"
|
||||
data-test="remove-control-button"
|
||||
@@ -40,9 +42,7 @@ export default function Option(props: OptionProps) {
|
||||
>
|
||||
<Icon name="x-small" color={theme.colors.grayscale.light1} />
|
||||
</CloseContainer>
|
||||
<Label data-test="control-label">
|
||||
<ColumnOption column={props.column} showType />
|
||||
</Label>
|
||||
<Label data-test="control-label">{props.children}</Label>
|
||||
{props.withCaret && (
|
||||
<CaretContainer>
|
||||
<Icon name="caret-right" color={theme.colors.grayscale.light1} />
|
||||
|
||||
@@ -25,15 +25,25 @@ import {
|
||||
} from 'react-dnd';
|
||||
import { DragContainer } from 'src/explore/components/OptionControls';
|
||||
import Option from './Option';
|
||||
import { OptionProps, GroupByItemInterface, GroupByItemType } from '../types';
|
||||
import { OptionProps, OptionItemInterface } from '../types';
|
||||
import { DndItemType } from '../../../DndItemType';
|
||||
|
||||
export default function OptionWrapper(props: OptionProps) {
|
||||
const { index, onShiftOptions } = props;
|
||||
export default function OptionWrapper(
|
||||
props: OptionProps & { type: DndItemType },
|
||||
) {
|
||||
const {
|
||||
index,
|
||||
type,
|
||||
onShiftOptions,
|
||||
clickClose,
|
||||
withCaret,
|
||||
children,
|
||||
} = props;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const item: GroupByItemInterface = {
|
||||
const item: OptionItemInterface = {
|
||||
dragIndex: index,
|
||||
type: GroupByItemType,
|
||||
type,
|
||||
};
|
||||
const [, drag] = useDrag({
|
||||
item,
|
||||
@@ -43,9 +53,9 @@ export default function OptionWrapper(props: OptionProps) {
|
||||
});
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: GroupByItemType,
|
||||
accept: type,
|
||||
|
||||
hover: (item: GroupByItemInterface, monitor: DropTargetMonitor) => {
|
||||
hover: (item: OptionItemInterface, monitor: DropTargetMonitor) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
@@ -89,8 +99,15 @@ export default function OptionWrapper(props: OptionProps) {
|
||||
drag(drop(ref));
|
||||
|
||||
return (
|
||||
<DragContainer ref={ref}>
|
||||
<Option {...props} />
|
||||
<DragContainer ref={ref} {...props}>
|
||||
<Option
|
||||
index={index}
|
||||
clickClose={clickClose}
|
||||
onShiftOptions={onShiftOptions}
|
||||
withCaret={withCaret}
|
||||
>
|
||||
{children}
|
||||
</Option>
|
||||
</DragContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default } from './DndColumnSelectLabel';
|
||||
export { default } from './DndSelectLabel';
|
||||
export * from './DndColumnSelect';
|
||||
export * from './DndFilterSelect';
|
||||
|
||||
@@ -16,19 +16,51 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import { ReactNode } from 'react';
|
||||
import { AdhocFilter } from '@superset-ui/core';
|
||||
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
import { DatasourcePanelDndItem } from '../../DatasourcePanel/types';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
export interface OptionProps {
|
||||
column: ColumnMeta;
|
||||
children: ReactNode;
|
||||
index: number;
|
||||
clickClose: (index: number) => void;
|
||||
onShiftOptions: (dragIndex: number, hoverIndex: number) => void;
|
||||
withCaret?: boolean;
|
||||
}
|
||||
|
||||
export const GroupByItemType = 'groupByItem';
|
||||
|
||||
export interface GroupByItemInterface {
|
||||
type: typeof GroupByItemType;
|
||||
export interface OptionItemInterface {
|
||||
type: string;
|
||||
dragIndex: number;
|
||||
}
|
||||
|
||||
export interface LabelProps<T = string[] | string> {
|
||||
name: string;
|
||||
value?: T;
|
||||
onChange: (value?: T) => void;
|
||||
options: { string: ColumnMeta };
|
||||
}
|
||||
|
||||
export interface DndColumnSelectProps<
|
||||
T = string[] | string,
|
||||
O = string[] | string
|
||||
> extends LabelProps<T> {
|
||||
values?: O;
|
||||
onDrop: (item: DatasourcePanelDndItem) => void;
|
||||
canDrop: (item: DatasourcePanelDndItem) => boolean;
|
||||
valuesRenderer: () => ReactNode;
|
||||
accept: DndItemType | DndItemType[];
|
||||
}
|
||||
|
||||
export type FilterOptionValueType = Record<string, any> | AdhocFilter;
|
||||
export interface DndFilterSelectProps {
|
||||
name: string;
|
||||
value: FilterOptionValueType[];
|
||||
columns: ColumnMeta[];
|
||||
datasource: Record<string, any>;
|
||||
formData: Record<string, any>;
|
||||
savedMetrics: Metric[];
|
||||
onChange: (filters: FilterOptionValueType[]) => void;
|
||||
options: { string: ColumnMeta };
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
|
||||
export class OptionSelector {
|
||||
groupByOptions: ColumnMeta[];
|
||||
values: ColumnMeta[];
|
||||
|
||||
options: { string: ColumnMeta };
|
||||
|
||||
@@ -27,18 +27,18 @@ export class OptionSelector {
|
||||
|
||||
constructor(
|
||||
options: { string: ColumnMeta },
|
||||
values: string[] | string | null,
|
||||
initialValues?: string[] | string,
|
||||
) {
|
||||
this.options = options;
|
||||
let groupByValues: string[];
|
||||
if (Array.isArray(values)) {
|
||||
groupByValues = values;
|
||||
let values: string[];
|
||||
if (Array.isArray(initialValues)) {
|
||||
values = initialValues;
|
||||
this.isArray = true;
|
||||
} else {
|
||||
groupByValues = values ? [values] : [];
|
||||
values = initialValues ? [initialValues] : [];
|
||||
this.isArray = false;
|
||||
}
|
||||
this.groupByOptions = groupByValues
|
||||
this.values = values
|
||||
.map(value => {
|
||||
if (value in options) {
|
||||
return options[value];
|
||||
@@ -50,37 +50,32 @@ export class OptionSelector {
|
||||
|
||||
add(value: string) {
|
||||
if (value in this.options) {
|
||||
this.groupByOptions.push(this.options[value]);
|
||||
this.values.push(this.options[value]);
|
||||
}
|
||||
}
|
||||
|
||||
del(idx: number) {
|
||||
this.groupByOptions.splice(idx, 1);
|
||||
this.values.splice(idx, 1);
|
||||
}
|
||||
|
||||
replace(idx: number, value: string) {
|
||||
if (this.groupByOptions[idx]) {
|
||||
this.groupByOptions[idx] = this.options[value];
|
||||
if (this.values[idx]) {
|
||||
this.values[idx] = this.options[value];
|
||||
}
|
||||
}
|
||||
|
||||
swap(a: number, b: number) {
|
||||
[this.groupByOptions[a], this.groupByOptions[b]] = [
|
||||
this.groupByOptions[b],
|
||||
this.groupByOptions[a],
|
||||
];
|
||||
[this.values[a], this.values[b]] = [this.values[b], this.values[a]];
|
||||
}
|
||||
|
||||
has(groupBy: string): boolean {
|
||||
return !!this.getValues()?.includes(groupBy);
|
||||
}
|
||||
|
||||
getValues(): string[] | string | null {
|
||||
getValues(): string[] | string | undefined {
|
||||
if (!this.isArray) {
|
||||
return this.groupByOptions.length > 0
|
||||
? this.groupByOptions[0].column_name
|
||||
: null;
|
||||
return this.values.length > 0 ? this.values[0].column_name : undefined;
|
||||
}
|
||||
return this.groupByOptions.map(option => option.column_name);
|
||||
return this.values.map(option => option.column_name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user