mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04: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:
parent
3970d7316b
commit
7b370e6f17
670
superset-frontend/package-lock.json
generated
670
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -65,34 +65,34 @@
|
||||
"@babel/runtime-corejs3": "^7.12.5",
|
||||
"@data-ui/sparkline": "^0.0.84",
|
||||
"@emotion/core": "^10.0.35",
|
||||
"@superset-ui/chart-controls": "^0.17.13",
|
||||
"@superset-ui/core": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.13",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.13",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.13",
|
||||
"@superset-ui/chart-controls": "^0.17.14",
|
||||
"@superset-ui/core": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.14",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.14",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.14",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.13",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.17.13",
|
||||
"@superset-ui/plugin-chart-table": "^0.17.13",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.13",
|
||||
"@superset-ui/preset-chart-xy": "^0.17.13",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-table": "^0.17.14",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.14",
|
||||
"@superset-ui/preset-chart-xy": "^0.17.14",
|
||||
"@vx/responsive": "^0.0.195",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"antd": "^4.9.4",
|
||||
|
@ -23,6 +23,6 @@ export default function setupSupersetClient() {
|
||||
// The following is needed to mock out SupersetClient requests
|
||||
// including CSRF authentication and initialization
|
||||
global.FormData = window.FormData; // used by SupersetClient
|
||||
fetchMock.get('glob:*superset/csrf_token/*', { csrf_token: '1234' });
|
||||
fetchMock.get('glob:*/api/v1/security/csrf_token/*', { result: '1234' });
|
||||
SupersetClient.configure({ protocol: 'http', host: 'localhost' }).init();
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
import Control from 'src/explore/components/Control';
|
||||
import DatasourcePanelDragWrapper from './DatasourcePanelDragWrapper';
|
||||
import { DatasourcePanelDndType } from './types';
|
||||
import { DndItemType } from '../DndItemType';
|
||||
|
||||
interface DatasourceControl extends ControlConfig {
|
||||
datasource?: DatasourceMeta;
|
||||
@ -213,8 +213,8 @@ export default function DataSourcePanel({
|
||||
<LabelContainer key={m.metric_name} className="column">
|
||||
{enableExploreDnd ? (
|
||||
<DatasourcePanelDragWrapper
|
||||
metricOrColumnName={m.metric_name}
|
||||
type={DatasourcePanelDndType.METRIC}
|
||||
value={m}
|
||||
type={DndItemType.Metric}
|
||||
>
|
||||
<MetricOption metric={m} showType />
|
||||
</DatasourcePanelDragWrapper>
|
||||
@ -235,8 +235,8 @@ export default function DataSourcePanel({
|
||||
<LabelContainer key={col.column_name} className="column">
|
||||
{enableExploreDnd ? (
|
||||
<DatasourcePanelDragWrapper
|
||||
metricOrColumnName={col.column_name}
|
||||
type={DatasourcePanelDndType.COLUMN}
|
||||
value={col}
|
||||
type={DndItemType.Column}
|
||||
>
|
||||
<ColumnOption column={col} showType />
|
||||
</DatasourcePanelDragWrapper>
|
||||
|
@ -42,7 +42,7 @@ export default function DatasourcePanelDragWrapper(
|
||||
) {
|
||||
const [, drag] = useDrag({
|
||||
item: {
|
||||
metricOrColumnName: props.metricOrColumnName,
|
||||
value: props.value,
|
||||
type: props.type,
|
||||
},
|
||||
});
|
||||
|
@ -16,15 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export enum DatasourcePanelDndType {
|
||||
// todo: The new `metric` conflicts with the existing metric type
|
||||
METRIC = 'datasource-panel-metric',
|
||||
COLUMN = 'column',
|
||||
}
|
||||
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
import { DndItemType } from '../DndItemType';
|
||||
|
||||
export type DndItemValue = ColumnMeta | Metric;
|
||||
|
||||
export interface DatasourcePanelDndItem {
|
||||
metricOrColumnName: string;
|
||||
type:
|
||||
| typeof DatasourcePanelDndType.METRIC
|
||||
| typeof DatasourcePanelDndType.COLUMN;
|
||||
value: DndItemValue;
|
||||
type: DndItemType;
|
||||
}
|
||||
|
@ -16,7 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const OPTION_TYPES = {
|
||||
metric: 'metric',
|
||||
filter: 'filter',
|
||||
};
|
||||
|
||||
/**
|
||||
* All possible draggable items for the chart controls.
|
||||
*/
|
||||
export enum DndItemType {
|
||||
// an existing column in table
|
||||
Column = 'column',
|
||||
// a selected column option in ColumnSelectControl
|
||||
ColumnOption = 'columnOption',
|
||||
// an adhoc column option in ColumnSelectControl
|
||||
AdhocColumnOption = 'adhocColumn',
|
||||
|
||||
// a saved metric
|
||||
Metric = 'metric',
|
||||
// a selected saved metric in MetricsControl
|
||||
MetricOption = 'metricOption',
|
||||
// an adhoc metric option in MetricsControl
|
||||
AdhocMetricOption = 'adhocMetric',
|
||||
|
||||
// an adhoc filter option
|
||||
FilterOption = 'filterOption',
|
||||
}
|
@ -26,6 +26,7 @@ import {
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
|
||||
import AdhocMetric from './controls/MetricControl/AdhocMetric';
|
||||
|
||||
export const DragContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit}px;
|
||||
@ -35,7 +36,7 @@ export const DragContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const OptionControlContainer = styled.div<{
|
||||
isAdhoc?: boolean;
|
||||
withCaret?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -44,7 +45,7 @@ export const OptionControlContainer = styled.div<{
|
||||
height: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light3};
|
||||
border-radius: 3px;
|
||||
cursor: ${({ isAdhoc }) => (isAdhoc ? 'pointer' : 'default')};
|
||||
cursor: ${({ withCaret }) => (withCaret ? 'pointer' : 'default')};
|
||||
`;
|
||||
|
||||
export const Label = styled.div`
|
||||
@ -159,10 +160,11 @@ interface DragItem {
|
||||
export const OptionControlLabel = ({
|
||||
label,
|
||||
savedMetric,
|
||||
adhocMetric,
|
||||
onRemove,
|
||||
onMoveLabel,
|
||||
onDropLabel,
|
||||
isAdhoc,
|
||||
withCaret,
|
||||
isFunction,
|
||||
type,
|
||||
index,
|
||||
@ -171,10 +173,11 @@ export const OptionControlLabel = ({
|
||||
}: {
|
||||
label: string | React.ReactNode;
|
||||
savedMetric?: savedMetricType;
|
||||
adhocMetric?: AdhocMetric;
|
||||
onRemove: () => void;
|
||||
onMoveLabel: (dragIndex: number, hoverIndex: number) => void;
|
||||
onDropLabel: () => void;
|
||||
isAdhoc?: boolean;
|
||||
withCaret?: boolean;
|
||||
isFunction?: boolean;
|
||||
isDraggable?: boolean;
|
||||
type: string;
|
||||
@ -231,7 +234,11 @@ export const OptionControlLabel = ({
|
||||
},
|
||||
});
|
||||
const [, drag] = useDrag({
|
||||
item: { type, index },
|
||||
item: {
|
||||
type,
|
||||
index,
|
||||
value: savedMetric?.metric_name ? savedMetric : adhocMetric,
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
@ -246,7 +253,7 @@ export const OptionControlLabel = ({
|
||||
|
||||
const getOptionControlContent = () => (
|
||||
<OptionControlContainer
|
||||
isAdhoc={isAdhoc}
|
||||
withCaret={withCaret}
|
||||
data-test="option-label"
|
||||
{...props}
|
||||
>
|
||||
@ -272,7 +279,7 @@ export const OptionControlLabel = ({
|
||||
`)}
|
||||
/>
|
||||
)}
|
||||
{isAdhoc && (
|
||||
{withCaret && (
|
||||
<CaretContainer>
|
||||
<Icon name="caret-right" color={theme.colors.grayscale.light1} />
|
||||
</CaretContainer>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,9 @@ import PropTypes from 'prop-types';
|
||||
import columnType from 'src/explore/propTypes/columnType';
|
||||
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||
import { OptionControlLabel } from 'src/explore/components/OptionControls';
|
||||
import { OPTION_TYPES } from 'src/explore/components/optionTypes';
|
||||
import AdhocFilterPopoverTrigger from './AdhocFilterPopoverTrigger';
|
||||
import AdhocFilter from './AdhocFilter';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
const propTypes = {
|
||||
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
||||
@ -67,8 +67,8 @@ const AdhocFilterOption = ({
|
||||
onMoveLabel={onMoveLabel}
|
||||
onDropLabel={onDropLabel}
|
||||
index={index}
|
||||
type={OPTION_TYPES.filter}
|
||||
isAdhoc
|
||||
type={DndItemType.FilterOption}
|
||||
withCaret
|
||||
isExtra={adhocFilter.isExtra}
|
||||
/>
|
||||
</AdhocFilterPopoverTrigger>
|
||||
|
@ -18,21 +18,21 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import Popover from 'src/common/components/Popover';
|
||||
import columnType from 'src/explore/propTypes/columnType';
|
||||
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||
import { OptionSortType } from 'src/explore/types';
|
||||
import AdhocFilterEditPopover from './AdhocFilterEditPopover';
|
||||
import AdhocFilter from './AdhocFilter';
|
||||
|
||||
interface AdhocFilterPopoverTriggerProps {
|
||||
adhocFilter: AdhocFilter;
|
||||
options:
|
||||
| typeof columnType[]
|
||||
| { saved_metric_name: string }[]
|
||||
| typeof adhocMetricType[];
|
||||
options: OptionSortType[];
|
||||
datasource: Record<string, any>;
|
||||
onFilterEdit: () => void;
|
||||
onFilterEdit: (editedFilter: AdhocFilter) => void;
|
||||
partitionColumn?: string;
|
||||
createNew?: boolean;
|
||||
isControlledComponent?: boolean;
|
||||
visible?: boolean;
|
||||
togglePopover?: (visible: boolean) => void;
|
||||
closePopover?: () => void;
|
||||
}
|
||||
|
||||
interface AdhocFilterPopoverTriggerState {
|
||||
@ -68,7 +68,19 @@ class AdhocFilterPopoverTrigger extends React.PureComponent<
|
||||
}
|
||||
|
||||
render() {
|
||||
const { adhocFilter } = this.props;
|
||||
const { adhocFilter, isControlledComponent } = this.props;
|
||||
|
||||
const { visible, togglePopover, closePopover } = isControlledComponent
|
||||
? {
|
||||
visible: this.props.visible,
|
||||
togglePopover: this.props.togglePopover,
|
||||
closePopover: this.props.closePopover,
|
||||
}
|
||||
: {
|
||||
visible: this.state.popoverVisible,
|
||||
togglePopover: this.togglePopover,
|
||||
closePopover: this.closePopover,
|
||||
};
|
||||
const overlayContent = (
|
||||
<AdhocFilterEditPopover
|
||||
adhocFilter={adhocFilter}
|
||||
@ -76,7 +88,7 @@ class AdhocFilterPopoverTrigger extends React.PureComponent<
|
||||
datasource={this.props.datasource}
|
||||
partitionColumn={this.props.partitionColumn}
|
||||
onResize={this.onPopoverResize}
|
||||
onClose={this.closePopover}
|
||||
onClose={closePopover}
|
||||
onChange={this.props.onFilterEdit}
|
||||
/>
|
||||
);
|
||||
@ -86,9 +98,9 @@ class AdhocFilterPopoverTrigger extends React.PureComponent<
|
||||
placement="right"
|
||||
trigger="click"
|
||||
content={overlayContent}
|
||||
defaultVisible={this.state.popoverVisible}
|
||||
visible={this.state.popoverVisible}
|
||||
onVisibleChange={this.togglePopover}
|
||||
defaultVisible={visible}
|
||||
visible={visible}
|
||||
onVisibleChange={togglePopover}
|
||||
destroyTooltipOnHide={this.props.createNew}
|
||||
>
|
||||
{this.props.children}
|
||||
|
@ -20,10 +20,10 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import columnType from 'src/explore/propTypes/columnType';
|
||||
import { OptionControlLabel } from 'src/explore/components/OptionControls';
|
||||
import { OPTION_TYPES } from 'src/explore/components/optionTypes';
|
||||
import AdhocMetric from './AdhocMetric';
|
||||
import savedMetricType from './savedMetricType';
|
||||
import AdhocMetricPopoverTrigger from './AdhocMetricPopoverTrigger';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
const propTypes = {
|
||||
adhocMetric: PropTypes.instanceOf(AdhocMetric),
|
||||
@ -73,13 +73,14 @@ class AdhocMetricOption extends React.PureComponent {
|
||||
>
|
||||
<OptionControlLabel
|
||||
savedMetric={savedMetric}
|
||||
adhocMetric={adhocMetric}
|
||||
label={adhocMetric.label}
|
||||
onRemove={this.onRemoveMetric}
|
||||
onMoveLabel={onMoveLabel}
|
||||
onDropLabel={onDropLabel}
|
||||
index={index}
|
||||
type={OPTION_TYPES.metric}
|
||||
isAdhoc
|
||||
type={DndItemType.AdhocMetricOption}
|
||||
withCaret
|
||||
isFunction
|
||||
/>
|
||||
</AdhocMetricPopoverTrigger>
|
||||
|
@ -20,11 +20,11 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import columnType from 'src/explore/propTypes/columnType';
|
||||
import { OptionControlLabel } from 'src/explore/components/OptionControls';
|
||||
import { OPTION_TYPES } from 'src/explore/components/optionTypes';
|
||||
import AdhocMetricOption from './AdhocMetricOption';
|
||||
import AdhocMetric from './AdhocMetric';
|
||||
import savedMetricType from './savedMetricType';
|
||||
import adhocMetricType from './adhocMetricType';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
|
||||
const propTypes = {
|
||||
option: PropTypes.oneOfType([savedMetricType, adhocMetricType]).isRequired,
|
||||
@ -88,7 +88,7 @@ export default function MetricDefinitionValue({
|
||||
onRemove={onRemoveMetric}
|
||||
onMoveLabel={onMoveLabel}
|
||||
onDropLabel={onDropLabel}
|
||||
type={OPTION_TYPES.metric}
|
||||
type={DndItemType.FilterOption}
|
||||
index={index}
|
||||
isFunction
|
||||
/>
|
||||
|
@ -39,7 +39,10 @@ import VizTypeControl from './VizTypeControl';
|
||||
import MetricsControl from './MetricControl/MetricsControl';
|
||||
import AdhocFilterControl from './FilterControl/AdhocFilterControl';
|
||||
import FilterBoxItemControl from './FilterBoxItemControl';
|
||||
import DndColumnSelectControl from './DndColumnSelectControl';
|
||||
import DndColumnSelectControl, {
|
||||
DndColumnSelect,
|
||||
DndFilterSelect,
|
||||
} from './DndColumnSelectControl';
|
||||
|
||||
const controlMap = {
|
||||
AnnotationLayerControl,
|
||||
@ -52,6 +55,8 @@ const controlMap = {
|
||||
DatasourceControl,
|
||||
DateFilterControl,
|
||||
DndColumnSelectControl,
|
||||
DndColumnSelect,
|
||||
DndFilterSelect,
|
||||
FixedOrMetricControl,
|
||||
HiddenControl,
|
||||
SelectAsyncControl,
|
||||
|
@ -16,7 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryData, QueryFormData, AnnotationData } from '@superset-ui/core';
|
||||
import {
|
||||
QueryData,
|
||||
QueryFormData,
|
||||
AnnotationData,
|
||||
AdhocMetric,
|
||||
} from '@superset-ui/core';
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
|
||||
export { Slice, Chart } from 'src/types/Chart';
|
||||
|
||||
@ -45,3 +51,7 @@ export interface ChartState {
|
||||
triggerQuery: boolean;
|
||||
asyncJobId?: string;
|
||||
}
|
||||
|
||||
export type OptionSortType = Partial<
|
||||
ColumnMeta & AdhocMetric & { saved_metric_name: string }
|
||||
>;
|
||||
|
Loading…
Reference in New Issue
Block a user