mirror of https://github.com/apache/superset.git
feat(explore): adhoc column expressions [ID-3] (#17379)
* add support for adhoc columns to api and sqla model * fix some types * fix duplicates in column names * fix more lint * fix schema and dedup * clean up some logic * first pass at fixing viz.py * Add frontend support for adhoc columns * Add title edit * Fix showing custom title * Use column name as default value in sql editor * fix: Adds a loading message when needed in the Select component (#16531) * fix(tests): make parquet select deterministic with order by (#16570) * bump emotion to help with cache clobbering (#16559) * fix: Support Jinja template functions in global async queries (#16412) * Support Jinja template functions in async queries * Pylint * Add tests for async tasks * Remove redundant has_request_context check * fix: impersonate user label/tooltip (#16573) * docs: update for small typos (#16568) * feat: Add Aurora Data API engine spec (#16535) * feat: Add Aurora Data API engine spec * Fix lint * refactor: sql_json view endpoint: encapsulate ctas parameters (#16548) * refactor sql_json view endpoint: encapsulate ctas parameters * fix failed tests * fix failed tests and ci issues * refactor sql_json view endpoint: separate concern into ad hod method (#16595) * feat: Experimental cross-filter plugins (#16594) * fix:fix get permission function * feat: add cross filter chart in charts gallery under FF * chore(deps): bump superset-ui to 0.18.2 (#16601) * update type guard references * fix imports * update series_columns schema * Add changes that got lost in rebase * Use current columns name or expression as sql editor init value * add integration test and do minor fixes * Bump superset-ui * fix linting issue * bump superset-ui to 0.18.22 * resolve merge conflict * lint * fix select filter infinite loop * bump superset-ui to 0.18.23 * Fix auto setting column popover title * Enable adhoc columns only if UX_BETA enabled * put back removed test * Move popover height and width to constants * Refactor big ternary expression Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Elizabeth Thompson <eschutho@gmail.com> Co-authored-by: Rob DiCiuccio <rob.diciuccio@gmail.com> Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> Co-authored-by: joeADSP <75027008+joeADSP@users.noreply.github.com> Co-authored-by: ofekisr <35701650+ofekisr@users.noreply.github.com> Co-authored-by: simcha90 <56388545+simcha90@users.noreply.github.com>
This commit is contained in:
parent
5d3e1b5c2c
commit
e2a429b0c8
File diff suppressed because it is too large
Load Diff
|
@ -68,35 +68,35 @@
|
||||||
"@emotion/cache": "^11.4.0",
|
"@emotion/cache": "^11.4.0",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@superset-ui/chart-controls": "^0.18.20",
|
"@superset-ui/chart-controls": "^0.18.23",
|
||||||
"@superset-ui/core": "^0.18.20",
|
"@superset-ui/core": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-calendar": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-chord": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-chord": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-country-map": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-event-flow": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-force-directed": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-heatmap": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-histogram": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-horizon": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-map-box": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-partition": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-partition": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-rose": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-rose": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-sankey": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.23",
|
||||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.20",
|
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.23",
|
||||||
"@superset-ui/legacy-preset-chart-big-number": "^0.18.20",
|
"@superset-ui/legacy-preset-chart-big-number": "^0.18.23",
|
||||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
|
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
|
||||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.20",
|
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.23",
|
||||||
"@superset-ui/plugin-chart-echarts": "^0.18.20",
|
"@superset-ui/plugin-chart-echarts": "^0.18.23",
|
||||||
"@superset-ui/plugin-chart-pivot-table": "^0.18.20",
|
"@superset-ui/plugin-chart-pivot-table": "^0.18.23",
|
||||||
"@superset-ui/plugin-chart-table": "^0.18.20",
|
"@superset-ui/plugin-chart-table": "^0.18.23",
|
||||||
"@superset-ui/plugin-chart-word-cloud": "^0.18.20",
|
"@superset-ui/plugin-chart-word-cloud": "^0.18.23",
|
||||||
"@superset-ui/preset-chart-xy": "^0.18.20",
|
"@superset-ui/preset-chart-xy": "^0.18.23",
|
||||||
"@vx/responsive": "^0.0.195",
|
"@vx/responsive": "^0.0.195",
|
||||||
"abortcontroller-polyfill": "^1.1.9",
|
"abortcontroller-polyfill": "^1.1.9",
|
||||||
"antd": "^4.9.4",
|
"antd": "^4.9.4",
|
||||||
|
|
|
@ -17,15 +17,27 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { AdhocColumn, t, styled, css } from '@superset-ui/core';
|
||||||
|
import {
|
||||||
|
ColumnMeta,
|
||||||
|
isAdhocColumn,
|
||||||
|
isSavedExpression,
|
||||||
|
} from '@superset-ui/chart-controls';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components';
|
||||||
import { t, styled } from '@superset-ui/core';
|
|
||||||
|
|
||||||
import { Form, FormItem } from 'src/components/Form';
|
import { Form, FormItem } from 'src/components/Form';
|
||||||
|
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||||
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
import { POPOVER_INITIAL_HEIGHT } from 'src/explore/constants';
|
||||||
|
|
||||||
const StyledSelect = styled(Select)`
|
const StyledSelect = styled(Select)`
|
||||||
.metric-option {
|
.metric-option {
|
||||||
|
@ -41,29 +53,58 @@ const StyledSelect = styled(Select)`
|
||||||
|
|
||||||
interface ColumnSelectPopoverProps {
|
interface ColumnSelectPopoverProps {
|
||||||
columns: ColumnMeta[];
|
columns: ColumnMeta[];
|
||||||
editedColumn?: ColumnMeta;
|
editedColumn?: ColumnMeta | AdhocColumn;
|
||||||
onChange: (column: ColumnMeta) => void;
|
onChange: (column: ColumnMeta | AdhocColumn) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
setLabel: (title: string) => void;
|
||||||
|
getCurrentTab: (tab: string) => void;
|
||||||
|
label: string;
|
||||||
|
isAdhocColumnsEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getInitialColumnValues = (
|
||||||
|
editedColumn?: ColumnMeta | AdhocColumn,
|
||||||
|
): [AdhocColumn?, ColumnMeta?, ColumnMeta?] => {
|
||||||
|
if (!editedColumn) {
|
||||||
|
return [undefined, undefined, undefined];
|
||||||
|
}
|
||||||
|
if (isAdhocColumn(editedColumn)) {
|
||||||
|
return [editedColumn, undefined, undefined];
|
||||||
|
}
|
||||||
|
if (isSavedExpression(editedColumn)) {
|
||||||
|
return [undefined, editedColumn, undefined];
|
||||||
|
}
|
||||||
|
return [undefined, undefined, editedColumn];
|
||||||
|
};
|
||||||
|
|
||||||
const ColumnSelectPopover = ({
|
const ColumnSelectPopover = ({
|
||||||
columns,
|
columns,
|
||||||
editedColumn,
|
editedColumn,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
|
setLabel,
|
||||||
|
getCurrentTab,
|
||||||
|
label,
|
||||||
|
isAdhocColumnsEnabled,
|
||||||
}: ColumnSelectPopoverProps) => {
|
}: ColumnSelectPopoverProps) => {
|
||||||
|
const [initialLabel] = useState(label);
|
||||||
const [
|
const [
|
||||||
|
initialAdhocColumn,
|
||||||
initialCalculatedColumn,
|
initialCalculatedColumn,
|
||||||
initialSimpleColumn,
|
initialSimpleColumn,
|
||||||
] = editedColumn?.expression
|
] = getInitialColumnValues(editedColumn);
|
||||||
? [editedColumn, undefined]
|
|
||||||
: [undefined, editedColumn];
|
const [adhocColumn, setAdhocColumn] = useState<AdhocColumn | undefined>(
|
||||||
const [selectedCalculatedColumn, setSelectedCalculatedColumn] = useState(
|
initialAdhocColumn,
|
||||||
initialCalculatedColumn,
|
|
||||||
);
|
|
||||||
const [selectedSimpleColumn, setSelectedSimpleColumn] = useState(
|
|
||||||
initialSimpleColumn,
|
|
||||||
);
|
);
|
||||||
|
const [selectedCalculatedColumn, setSelectedCalculatedColumn] = useState<
|
||||||
|
ColumnMeta | undefined
|
||||||
|
>(initialCalculatedColumn);
|
||||||
|
const [selectedSimpleColumn, setSelectedSimpleColumn] = useState<
|
||||||
|
ColumnMeta | undefined
|
||||||
|
>(initialSimpleColumn);
|
||||||
|
|
||||||
|
const sqlEditorRef = useRef(null);
|
||||||
|
|
||||||
const [calculatedColumns, simpleColumns] = useMemo(
|
const [calculatedColumns, simpleColumns] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -81,6 +122,15 @@ const ColumnSelectPopover = ({
|
||||||
[columns],
|
[columns],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSqlExpressionChange = useCallback(
|
||||||
|
sqlExpression => {
|
||||||
|
setAdhocColumn({ label, sqlExpression } as AdhocColumn);
|
||||||
|
setSelectedSimpleColumn(undefined);
|
||||||
|
setSelectedCalculatedColumn(undefined);
|
||||||
|
},
|
||||||
|
[label],
|
||||||
|
);
|
||||||
|
|
||||||
const onCalculatedColumnChange = useCallback(
|
const onCalculatedColumnChange = useCallback(
|
||||||
selectedColumnName => {
|
selectedColumnName => {
|
||||||
const selectedColumn = calculatedColumns.find(
|
const selectedColumn = calculatedColumns.find(
|
||||||
|
@ -88,8 +138,12 @@ const ColumnSelectPopover = ({
|
||||||
);
|
);
|
||||||
setSelectedCalculatedColumn(selectedColumn);
|
setSelectedCalculatedColumn(selectedColumn);
|
||||||
setSelectedSimpleColumn(undefined);
|
setSelectedSimpleColumn(undefined);
|
||||||
|
setAdhocColumn(undefined);
|
||||||
|
setLabel(
|
||||||
|
selectedColumn?.verbose_name || selectedColumn?.column_name || '',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[calculatedColumns],
|
[calculatedColumns, setLabel],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSimpleColumnChange = useCallback(
|
const onSimpleColumnChange = useCallback(
|
||||||
|
@ -99,33 +153,79 @@ const ColumnSelectPopover = ({
|
||||||
);
|
);
|
||||||
setSelectedCalculatedColumn(undefined);
|
setSelectedCalculatedColumn(undefined);
|
||||||
setSelectedSimpleColumn(selectedColumn);
|
setSelectedSimpleColumn(selectedColumn);
|
||||||
|
setAdhocColumn(undefined);
|
||||||
|
setLabel(
|
||||||
|
selectedColumn?.verbose_name || selectedColumn?.column_name || '',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[simpleColumns],
|
[setLabel, simpleColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultActiveTabKey =
|
const defaultActiveTabKey = initialAdhocColumn
|
||||||
initialSimpleColumn || calculatedColumns.length === 0 ? 'simple' : 'saved';
|
? 'sqlExpression'
|
||||||
|
: initialSimpleColumn || calculatedColumns.length === 0
|
||||||
|
? 'simple'
|
||||||
|
: 'saved';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCurrentTab(defaultActiveTabKey);
|
||||||
|
}, [defaultActiveTabKey, getCurrentTab]);
|
||||||
|
|
||||||
const onSave = useCallback(() => {
|
const onSave = useCallback(() => {
|
||||||
const selectedColumn = selectedCalculatedColumn || selectedSimpleColumn;
|
if (adhocColumn && adhocColumn.label !== label) {
|
||||||
|
adhocColumn.label = label;
|
||||||
|
}
|
||||||
|
const selectedColumn =
|
||||||
|
adhocColumn || selectedCalculatedColumn || selectedSimpleColumn;
|
||||||
if (!selectedColumn) {
|
if (!selectedColumn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onChange(selectedColumn);
|
onChange(selectedColumn);
|
||||||
onClose();
|
onClose();
|
||||||
}, [onChange, onClose, selectedCalculatedColumn, selectedSimpleColumn]);
|
}, [
|
||||||
|
adhocColumn,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
selectedCalculatedColumn,
|
||||||
|
selectedSimpleColumn,
|
||||||
|
]);
|
||||||
|
|
||||||
const onResetStateAndClose = useCallback(() => {
|
const onResetStateAndClose = useCallback(() => {
|
||||||
setSelectedCalculatedColumn(initialCalculatedColumn);
|
setSelectedCalculatedColumn(initialCalculatedColumn);
|
||||||
setSelectedSimpleColumn(initialSimpleColumn);
|
setSelectedSimpleColumn(initialSimpleColumn);
|
||||||
|
setAdhocColumn(initialAdhocColumn);
|
||||||
onClose();
|
onClose();
|
||||||
}, [initialCalculatedColumn, initialSimpleColumn, onClose]);
|
}, [
|
||||||
|
initialAdhocColumn,
|
||||||
|
initialCalculatedColumn,
|
||||||
|
initialSimpleColumn,
|
||||||
|
onClose,
|
||||||
|
]);
|
||||||
|
|
||||||
const stateIsValid = selectedCalculatedColumn || selectedSimpleColumn;
|
const onTabChange = useCallback(
|
||||||
|
tab => {
|
||||||
|
getCurrentTab(tab);
|
||||||
|
// @ts-ignore
|
||||||
|
sqlEditorRef.current?.editor.focus();
|
||||||
|
},
|
||||||
|
[getCurrentTab],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSqlEditorFocus = useCallback(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
sqlEditorRef.current?.editor.resize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stateIsValid =
|
||||||
|
adhocColumn || selectedCalculatedColumn || selectedSimpleColumn;
|
||||||
const hasUnsavedChanges =
|
const hasUnsavedChanges =
|
||||||
|
initialLabel !== label ||
|
||||||
selectedCalculatedColumn?.column_name !==
|
selectedCalculatedColumn?.column_name !==
|
||||||
initialCalculatedColumn?.column_name ||
|
initialCalculatedColumn?.column_name ||
|
||||||
selectedSimpleColumn?.column_name !== initialSimpleColumn?.column_name;
|
selectedSimpleColumn?.column_name !== initialSimpleColumn?.column_name ||
|
||||||
|
adhocColumn?.sqlExpression !== initialAdhocColumn?.sqlExpression;
|
||||||
|
|
||||||
const savedExpressionsLabel = t('Saved expressions');
|
const savedExpressionsLabel = t('Saved expressions');
|
||||||
const simpleColumnsLabel = t('Column');
|
const simpleColumnsLabel = t('Column');
|
||||||
|
|
||||||
|
@ -134,8 +234,12 @@ const ColumnSelectPopover = ({
|
||||||
<Tabs
|
<Tabs
|
||||||
id="adhoc-metric-edit-tabs"
|
id="adhoc-metric-edit-tabs"
|
||||||
defaultActiveKey={defaultActiveTabKey}
|
defaultActiveKey={defaultActiveTabKey}
|
||||||
|
onChange={onTabChange}
|
||||||
className="adhoc-metric-edit-tabs"
|
className="adhoc-metric-edit-tabs"
|
||||||
allowOverflow
|
allowOverflow
|
||||||
|
css={css`
|
||||||
|
height: ${POPOVER_INITIAL_HEIGHT}px;
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<Tabs.TabPane key="saved" tab={t('Saved')}>
|
<Tabs.TabPane key="saved" tab={t('Saved')}>
|
||||||
<FormItem label={savedExpressionsLabel}>
|
<FormItem label={savedExpressionsLabel}>
|
||||||
|
@ -178,6 +282,28 @@ const ColumnSelectPopover = ({
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
{isAdhocColumnsEnabled && (
|
||||||
|
<Tabs.TabPane key="sqlExpression" tab={t('Custom SQL')}>
|
||||||
|
<SQLEditor
|
||||||
|
value={
|
||||||
|
adhocColumn?.sqlExpression ||
|
||||||
|
selectedSimpleColumn?.column_name ||
|
||||||
|
selectedCalculatedColumn?.expression
|
||||||
|
}
|
||||||
|
onFocus={onSqlEditorFocus}
|
||||||
|
showLoadingForImport
|
||||||
|
onChange={onSqlExpressionChange}
|
||||||
|
width="100%"
|
||||||
|
height={`${POPOVER_INITIAL_HEIGHT - 80}px`}
|
||||||
|
showGutter={false}
|
||||||
|
editorProps={{ $blockScrolling: true }}
|
||||||
|
enableLiveAutocompletion
|
||||||
|
className="filter-sql-editor"
|
||||||
|
wrapEnabled
|
||||||
|
ref={sqlEditorRef}
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div>
|
<div>
|
||||||
<Button buttonSize="small" onClick={onResetStateAndClose} cta>
|
<Button buttonSize="small" onClick={onResetStateAndClose} cta>
|
||||||
|
|
|
@ -16,16 +16,27 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
import {
|
||||||
|
AdhocColumn,
|
||||||
|
FeatureFlag,
|
||||||
|
isFeatureEnabled,
|
||||||
|
t,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import {
|
||||||
|
ColumnMeta,
|
||||||
|
isAdhocColumn,
|
||||||
|
isColumnMeta,
|
||||||
|
} from '@superset-ui/chart-controls';
|
||||||
import Popover from 'src/components/Popover';
|
import Popover from 'src/components/Popover';
|
||||||
import { ExplorePopoverContent } from 'src/explore/components/ExploreContentPopover';
|
import { ExplorePopoverContent } from 'src/explore/components/ExploreContentPopover';
|
||||||
import ColumnSelectPopover from './ColumnSelectPopover';
|
import ColumnSelectPopover from './ColumnSelectPopover';
|
||||||
|
import { DndColumnSelectPopoverTitle } from './DndColumnSelectPopoverTitle';
|
||||||
|
|
||||||
interface ColumnSelectPopoverTriggerProps {
|
interface ColumnSelectPopoverTriggerProps {
|
||||||
columns: ColumnMeta[];
|
columns: ColumnMeta[];
|
||||||
editedColumn?: ColumnMeta;
|
editedColumn?: ColumnMeta | AdhocColumn;
|
||||||
onColumnEdit: (editedColumn: ColumnMeta) => void;
|
onColumnEdit: (editedColumn: ColumnMeta | AdhocColumn) => void;
|
||||||
isControlledComponent?: boolean;
|
isControlledComponent?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
togglePopover?: (visible: boolean) => void;
|
togglePopover?: (visible: boolean) => void;
|
||||||
|
@ -33,6 +44,11 @@ interface ColumnSelectPopoverTriggerProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPopoverLabel = t('My column');
|
||||||
|
const editableTitleTab = 'sqlExpression';
|
||||||
|
|
||||||
|
const isAdhocColumnsEnabled = isFeatureEnabled(FeatureFlag.UX_BETA);
|
||||||
|
|
||||||
const ColumnSelectPopoverTrigger = ({
|
const ColumnSelectPopoverTrigger = ({
|
||||||
columns,
|
columns,
|
||||||
editedColumn,
|
editedColumn,
|
||||||
|
@ -41,7 +57,21 @@ const ColumnSelectPopoverTrigger = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ColumnSelectPopoverTriggerProps) => {
|
}: ColumnSelectPopoverTriggerProps) => {
|
||||||
|
const [popoverLabel, setPopoverLabel] = useState(defaultPopoverLabel);
|
||||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||||
|
const [isTitleEditDisabled, setIsTitleEditDisabled] = useState(true);
|
||||||
|
const [hasCustomLabel, setHasCustomLabel] = useState(false);
|
||||||
|
|
||||||
|
let initialPopoverLabel = defaultPopoverLabel;
|
||||||
|
if (editedColumn && isColumnMeta(editedColumn)) {
|
||||||
|
initialPopoverLabel = editedColumn.verbose_name || editedColumn.column_name;
|
||||||
|
} else if (editedColumn && isAdhocColumn(editedColumn)) {
|
||||||
|
initialPopoverLabel = editedColumn.label || defaultPopoverLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPopoverLabel(initialPopoverLabel);
|
||||||
|
}, [initialPopoverLabel, popoverVisible]);
|
||||||
|
|
||||||
const togglePopover = useCallback((visible: boolean) => {
|
const togglePopover = useCallback((visible: boolean) => {
|
||||||
setPopoverVisible(visible);
|
setPopoverVisible(visible);
|
||||||
|
@ -67,6 +97,10 @@ const ColumnSelectPopoverTrigger = ({
|
||||||
handleClosePopover: closePopover,
|
handleClosePopover: closePopover,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCurrentTab = useCallback((tab: string) => {
|
||||||
|
setIsTitleEditDisabled(tab !== editableTitleTab);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const overlayContent = useMemo(
|
const overlayContent = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<ExplorePopoverContent>
|
<ExplorePopoverContent>
|
||||||
|
@ -75,10 +109,38 @@ const ColumnSelectPopoverTrigger = ({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onClose={handleClosePopover}
|
onClose={handleClosePopover}
|
||||||
onChange={onColumnEdit}
|
onChange={onColumnEdit}
|
||||||
|
label={popoverLabel}
|
||||||
|
setLabel={setPopoverLabel}
|
||||||
|
getCurrentTab={getCurrentTab}
|
||||||
|
isAdhocColumnsEnabled={isAdhocColumnsEnabled}
|
||||||
/>
|
/>
|
||||||
</ExplorePopoverContent>
|
</ExplorePopoverContent>
|
||||||
),
|
),
|
||||||
[columns, editedColumn, handleClosePopover, onColumnEdit],
|
[
|
||||||
|
columns,
|
||||||
|
editedColumn,
|
||||||
|
getCurrentTab,
|
||||||
|
handleClosePopover,
|
||||||
|
onColumnEdit,
|
||||||
|
popoverLabel,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLabelChange = useCallback((e: any) => {
|
||||||
|
setPopoverLabel(e.target.value);
|
||||||
|
setHasCustomLabel(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const popoverTitle = useMemo(
|
||||||
|
() => (
|
||||||
|
<DndColumnSelectPopoverTitle
|
||||||
|
title={popoverLabel}
|
||||||
|
onChange={onLabelChange}
|
||||||
|
isEditDisabled={isTitleEditDisabled}
|
||||||
|
hasCustomLabel={hasCustomLabel}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[hasCustomLabel, isTitleEditDisabled, onLabelChange, popoverLabel],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -89,6 +151,7 @@ const ColumnSelectPopoverTrigger = ({
|
||||||
defaultVisible={visible}
|
defaultVisible={visible}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onVisibleChange={handleTogglePopover}
|
onVisibleChange={handleTogglePopover}
|
||||||
|
title={isAdhocColumnsEnabled && popoverTitle}
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -17,8 +17,14 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { FeatureFlag, isFeatureEnabled, tn } from '@superset-ui/core';
|
import {
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
AdhocColumn,
|
||||||
|
FeatureFlag,
|
||||||
|
isFeatureEnabled,
|
||||||
|
tn,
|
||||||
|
QueryFormColumn,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import { ColumnMeta, isColumnMeta } from '@superset-ui/chart-controls';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
|
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
|
||||||
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
|
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
|
||||||
|
@ -29,7 +35,7 @@ import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate';
|
||||||
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
|
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
|
||||||
import { DndControlProps } from './types';
|
import { DndControlProps } from './types';
|
||||||
|
|
||||||
export type DndColumnSelectProps = DndControlProps<string> & {
|
export type DndColumnSelectProps = DndControlProps<QueryFormColumn> & {
|
||||||
options: Record<string, ColumnMeta>;
|
options: Record<string, ColumnMeta>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,7 +129,8 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
|
||||||
Object.values(options).filter(
|
Object.values(options).filter(
|
||||||
col =>
|
col =>
|
||||||
!optionSelector.values
|
!optionSelector.values
|
||||||
.map(val => val.column_name)
|
.filter(isColumnMeta)
|
||||||
|
.map((val: ColumnMeta) => val.column_name)
|
||||||
.includes(col.column_name),
|
.includes(col.column_name),
|
||||||
),
|
),
|
||||||
[optionSelector.values, options],
|
[optionSelector.values, options],
|
||||||
|
@ -136,7 +143,11 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
|
||||||
<ColumnSelectPopoverTrigger
|
<ColumnSelectPopoverTrigger
|
||||||
columns={popoverOptions}
|
columns={popoverOptions}
|
||||||
onColumnEdit={newColumn => {
|
onColumnEdit={newColumn => {
|
||||||
optionSelector.replace(idx, newColumn.column_name);
|
if (isColumnMeta(newColumn)) {
|
||||||
|
optionSelector.replace(idx, newColumn.column_name);
|
||||||
|
} else {
|
||||||
|
optionSelector.replace(idx, newColumn as AdhocColumn);
|
||||||
|
}
|
||||||
onChange(optionSelector.getValues());
|
onChange(optionSelector.getValues());
|
||||||
}}
|
}}
|
||||||
editedColumn={column}
|
editedColumn={column}
|
||||||
|
@ -177,8 +188,12 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const addNewColumnWithPopover = useCallback(
|
const addNewColumnWithPopover = useCallback(
|
||||||
(newColumn: ColumnMeta) => {
|
(newColumn: ColumnMeta | AdhocColumn) => {
|
||||||
optionSelector.add(newColumn.column_name);
|
if (isColumnMeta(newColumn)) {
|
||||||
|
optionSelector.add(newColumn.column_name);
|
||||||
|
} else {
|
||||||
|
optionSelector.add(newColumn as AdhocColumn);
|
||||||
|
}
|
||||||
onChange(optionSelector.getValues());
|
onChange(optionSelector.getValues());
|
||||||
},
|
},
|
||||||
[onChange, optionSelector],
|
[onChange, optionSelector],
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* 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, { useCallback, useState } from 'react';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
|
import { Input } from 'src/common/components';
|
||||||
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
|
||||||
|
export const DndColumnSelectPopoverTitle = ({
|
||||||
|
title,
|
||||||
|
onChange,
|
||||||
|
isEditDisabled,
|
||||||
|
hasCustomLabel,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
const onMouseOver = useCallback(() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseOut = useCallback(() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
setIsEditMode(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onBlur = useCallback(() => {
|
||||||
|
setIsEditMode(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onInputBlur = useCallback(
|
||||||
|
e => {
|
||||||
|
if (e.target.value === '') {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
onBlur();
|
||||||
|
},
|
||||||
|
[onBlur, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultLabel = t('My column');
|
||||||
|
|
||||||
|
if (isEditDisabled) {
|
||||||
|
return <span>{title || defaultLabel}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEditMode ? (
|
||||||
|
<Input
|
||||||
|
className="metric-edit-popover-label-input"
|
||||||
|
type="text"
|
||||||
|
placeholder={title}
|
||||||
|
value={hasCustomLabel ? title : ''}
|
||||||
|
autoFocus
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onInputBlur}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tooltip placement="top" title={t('Click to edit label')}>
|
||||||
|
<span
|
||||||
|
className="AdhocMetricEditPopoverTitle inline-editable"
|
||||||
|
data-test="AdhocMetricEditTitle#trigger"
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseOut={onMouseOut}
|
||||||
|
onClick={onClick}
|
||||||
|
onBlur={onBlur}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{title || defaultLabel}
|
||||||
|
|
||||||
|
<i
|
||||||
|
className="fa fa-pencil"
|
||||||
|
style={{ color: isHovered ? 'black' : 'grey' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -31,7 +31,7 @@ import {
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
||||||
import { styled } from '@superset-ui/core';
|
import { styled } from '@superset-ui/core';
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
import { ColumnMeta, isAdhocColumn } from '@superset-ui/chart-controls';
|
||||||
import Option from './Option';
|
import Option from './Option';
|
||||||
|
|
||||||
export const OptionLabel = styled.div`
|
export const OptionLabel = styled.div`
|
||||||
|
@ -135,14 +135,20 @@ export default function OptionWrapper(
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnOption = () => (
|
const ColumnOption = () => {
|
||||||
<StyledColumnOption
|
const transformedCol =
|
||||||
column={column as ColumnMeta}
|
column && isAdhocColumn(column)
|
||||||
labelRef={labelRef}
|
? { verbose_name: column.label, expression: column.sqlExpression }
|
||||||
showTooltip={!!shouldShowTooltip}
|
: column;
|
||||||
showType
|
return (
|
||||||
/>
|
<StyledColumnOption
|
||||||
);
|
column={transformedCol as ColumnMeta}
|
||||||
|
labelRef={labelRef}
|
||||||
|
showTooltip={!!shouldShowTooltip}
|
||||||
|
showType
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Label = () => {
|
const Label = () => {
|
||||||
if (label) {
|
if (label) {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { JsonValue } from '@superset-ui/core';
|
import { AdhocColumn, JsonValue } from '@superset-ui/core';
|
||||||
import { ControlComponentProps } from 'src/explore/components/Control';
|
import { ControlComponentProps } from 'src/explore/components/Control';
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export interface OptionProps {
|
||||||
index: number;
|
index: number;
|
||||||
label?: string;
|
label?: string;
|
||||||
tooltipTitle?: string;
|
tooltipTitle?: string;
|
||||||
column?: ColumnMeta;
|
column?: ColumnMeta | AdhocColumn;
|
||||||
clickClose: (index: number) => void;
|
clickClose: (index: number) => void;
|
||||||
withCaret?: boolean;
|
withCaret?: boolean;
|
||||||
isExtra?: boolean;
|
isExtra?: boolean;
|
||||||
|
|
|
@ -16,11 +16,25 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
import { ColumnMeta, isColumnMeta } from '@superset-ui/chart-controls';
|
||||||
import { ensureIsArray } from '@superset-ui/core';
|
import {
|
||||||
|
AdhocColumn,
|
||||||
|
ensureIsArray,
|
||||||
|
QueryFormColumn,
|
||||||
|
isPhysicalColumn,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
|
const getColumnNameOrAdhocColumn = (
|
||||||
|
column: ColumnMeta | AdhocColumn,
|
||||||
|
): QueryFormColumn => {
|
||||||
|
if (isColumnMeta(column)) {
|
||||||
|
return column.column_name;
|
||||||
|
}
|
||||||
|
return column as AdhocColumn;
|
||||||
|
};
|
||||||
|
|
||||||
export class OptionSelector {
|
export class OptionSelector {
|
||||||
values: ColumnMeta[];
|
values: (ColumnMeta | AdhocColumn)[];
|
||||||
|
|
||||||
options: Record<string, ColumnMeta>;
|
options: Record<string, ColumnMeta>;
|
||||||
|
|
||||||
|
@ -29,23 +43,28 @@ export class OptionSelector {
|
||||||
constructor(
|
constructor(
|
||||||
options: Record<string, ColumnMeta>,
|
options: Record<string, ColumnMeta>,
|
||||||
multi: boolean,
|
multi: boolean,
|
||||||
initialValues?: string[] | string | null,
|
initialValues?: QueryFormColumn[] | QueryFormColumn | null,
|
||||||
) {
|
) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.multi = multi;
|
this.multi = multi;
|
||||||
this.values = ensureIsArray(initialValues)
|
this.values = ensureIsArray(initialValues)
|
||||||
.map(value => {
|
.map(value => {
|
||||||
if (value && value in options) {
|
if (value && isPhysicalColumn(value) && value in options) {
|
||||||
return options[value];
|
return options[value];
|
||||||
}
|
}
|
||||||
|
if (!isPhysicalColumn(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as ColumnMeta[];
|
.filter(Boolean) as ColumnMeta[];
|
||||||
}
|
}
|
||||||
|
|
||||||
add(value: string) {
|
add(value: QueryFormColumn) {
|
||||||
if (value in this.options) {
|
if (isPhysicalColumn(value) && value in this.options) {
|
||||||
this.values.push(this.options[value]);
|
this.values.push(this.options[value]);
|
||||||
|
} else if (!isPhysicalColumn(value)) {
|
||||||
|
this.values.push(value as AdhocColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,9 +72,9 @@ export class OptionSelector {
|
||||||
this.values.splice(idx, 1);
|
this.values.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(idx: number, value: string) {
|
replace(idx: number, value: QueryFormColumn) {
|
||||||
if (this.values[idx]) {
|
if (this.values[idx]) {
|
||||||
this.values[idx] = this.options[value];
|
this.values[idx] = isPhysicalColumn(value) ? this.options[value] : value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,14 +82,27 @@ export class OptionSelector {
|
||||||
[this.values[a], this.values[b]] = [this.values[b], this.values[a]];
|
[this.values[a], this.values[b]] = [this.values[b], this.values[a]];
|
||||||
}
|
}
|
||||||
|
|
||||||
has(value: string): boolean {
|
has(value: QueryFormColumn): boolean {
|
||||||
return ensureIsArray(this.getValues()).includes(value);
|
return this.values.some(col => {
|
||||||
|
if (isPhysicalColumn(value)) {
|
||||||
|
return (
|
||||||
|
(col as ColumnMeta).column_name === value ||
|
||||||
|
(col as AdhocColumn).label === value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(col as ColumnMeta).column_name === value.label ||
|
||||||
|
(col as AdhocColumn).label === value.label
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getValues(): string[] | string | undefined {
|
getValues(): QueryFormColumn[] | QueryFormColumn | undefined {
|
||||||
if (!this.multi) {
|
if (!this.multi) {
|
||||||
return this.values.length > 0 ? this.values[0].column_name : undefined;
|
return this.values.length > 0
|
||||||
|
? getColumnNameOrAdhocColumn(this.values[0])
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
return this.values.map(option => option.column_name);
|
return this.values.map(getColumnNameOrAdhocColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,10 @@ import AdhocFilter, {
|
||||||
import AdhocFilterEditPopoverSimpleTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent';
|
import AdhocFilterEditPopoverSimpleTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent';
|
||||||
import AdhocFilterEditPopoverSqlTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent';
|
import AdhocFilterEditPopoverSqlTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent';
|
||||||
import columnType from 'src/explore/components/controls/FilterControl/columnType';
|
import columnType from 'src/explore/components/controls/FilterControl/columnType';
|
||||||
|
import {
|
||||||
|
POPOVER_INITIAL_HEIGHT,
|
||||||
|
POPOVER_INITIAL_WIDTH,
|
||||||
|
} from 'src/explore/constants';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
|
||||||
|
@ -55,9 +59,6 @@ const ResizeIcon = styled.i`
|
||||||
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const startingWidth = 320;
|
|
||||||
const startingHeight = 240;
|
|
||||||
|
|
||||||
const FilterPopoverContentContainer = styled.div`
|
const FilterPopoverContentContainer = styled.div`
|
||||||
.adhoc-filter-edit-tabs > .nav-tabs {
|
.adhoc-filter-edit-tabs > .nav-tabs {
|
||||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
@ -98,8 +99,8 @@ export default class AdhocFilterEditPopover extends React.Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
adhocFilter: this.props.adhocFilter,
|
adhocFilter: this.props.adhocFilter,
|
||||||
width: startingWidth,
|
width: POPOVER_INITIAL_WIDTH,
|
||||||
height: startingHeight,
|
height: POPOVER_INITIAL_HEIGHT,
|
||||||
activeKey: this.props?.adhocFilter?.expressionType || 'SIMPLE',
|
activeKey: this.props?.adhocFilter?.expressionType || 'SIMPLE',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -137,11 +138,11 @@ export default class AdhocFilterEditPopover extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
width: Math.max(
|
width: Math.max(
|
||||||
this.dragStartWidth + (e.clientX - this.dragStartX),
|
this.dragStartWidth + (e.clientX - this.dragStartX),
|
||||||
startingWidth,
|
POPOVER_INITIAL_WIDTH,
|
||||||
),
|
),
|
||||||
height: Math.max(
|
height: Math.max(
|
||||||
this.dragStartHeight + (e.clientY - this.dragStartY) * 2,
|
this.dragStartHeight + (e.clientY - this.dragStartY) * 2,
|
||||||
startingHeight,
|
POPOVER_INITIAL_HEIGHT,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,11 @@ import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||||
import { noOp } from 'src/utils/common';
|
import { noOp } from 'src/utils/common';
|
||||||
|
|
||||||
import { AGGREGATES_OPTIONS } from 'src/explore/constants';
|
import {
|
||||||
|
AGGREGATES_OPTIONS,
|
||||||
|
POPOVER_INITIAL_HEIGHT,
|
||||||
|
POPOVER_INITIAL_WIDTH,
|
||||||
|
} from 'src/explore/constants';
|
||||||
import columnType from 'src/explore/components/controls/MetricControl/columnType';
|
import columnType from 'src/explore/components/controls/MetricControl/columnType';
|
||||||
import savedMetricType from 'src/explore/components/controls/MetricControl/savedMetricType';
|
import savedMetricType from 'src/explore/components/controls/MetricControl/savedMetricType';
|
||||||
import AdhocMetric, {
|
import AdhocMetric, {
|
||||||
|
@ -73,9 +77,6 @@ const StyledSelect = styled(Select)`
|
||||||
|
|
||||||
export const SAVED_TAB_KEY = 'SAVED';
|
export const SAVED_TAB_KEY = 'SAVED';
|
||||||
|
|
||||||
const startingWidth = 320;
|
|
||||||
const startingHeight = 240;
|
|
||||||
|
|
||||||
export default class AdhocMetricEditPopover extends React.PureComponent {
|
export default class AdhocMetricEditPopover extends React.PureComponent {
|
||||||
// "Saved" is a default tab unless there are no saved metrics for dataset
|
// "Saved" is a default tab unless there are no saved metrics for dataset
|
||||||
defaultActiveTabKey =
|
defaultActiveTabKey =
|
||||||
|
@ -103,8 +104,8 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
adhocMetric: this.props.adhocMetric,
|
adhocMetric: this.props.adhocMetric,
|
||||||
savedMetric: this.props.savedMetric,
|
savedMetric: this.props.savedMetric,
|
||||||
width: startingWidth,
|
width: POPOVER_INITIAL_WIDTH,
|
||||||
height: startingHeight,
|
height: POPOVER_INITIAL_HEIGHT,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mouseup', this.onMouseUp);
|
document.addEventListener('mouseup', this.onMouseUp);
|
||||||
|
@ -225,11 +226,11 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
|
||||||
this.setState({
|
this.setState({
|
||||||
width: Math.max(
|
width: Math.max(
|
||||||
this.dragStartWidth + (e.clientX - this.dragStartX),
|
this.dragStartWidth + (e.clientX - this.dragStartX),
|
||||||
startingWidth,
|
POPOVER_INITIAL_WIDTH,
|
||||||
),
|
),
|
||||||
height: Math.max(
|
height: Math.max(
|
||||||
this.dragStartHeight + (e.clientY - this.dragStartY) * 2,
|
this.dragStartHeight + (e.clientY - this.dragStartY) * 2,
|
||||||
startingHeight,
|
POPOVER_INITIAL_HEIGHT,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,3 +151,6 @@ export enum FILTER_BOX_MIGRATION_STATES {
|
||||||
export const FILTER_BOX_TRANSITION_SNOOZED_AT =
|
export const FILTER_BOX_TRANSITION_SNOOZED_AT =
|
||||||
'filter_box_transition_snoozed_at';
|
'filter_box_transition_snoozed_at';
|
||||||
export const FILTER_BOX_TRANSITION_SNOOZE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
export const FILTER_BOX_TRANSITION_SNOOZE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
|
export const POPOVER_INITIAL_HEIGHT = 240;
|
||||||
|
export const POPOVER_INITIAL_WIDTH = 320;
|
||||||
|
|
|
@ -16,7 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ensureIsArray, ExtraFormData, t, tn } from '@superset-ui/core';
|
import {
|
||||||
|
ensureIsArray,
|
||||||
|
ExtraFormData,
|
||||||
|
getColumnLabel,
|
||||||
|
t,
|
||||||
|
tn,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { FormItemProps } from 'antd/lib/form';
|
import { FormItemProps } from 'antd/lib/form';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components';
|
||||||
|
@ -62,15 +68,11 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
|
||||||
// so we can process it like this `JSON.stringify` or start to use `Immer`
|
// so we can process it like this `JSON.stringify` or start to use `Immer`
|
||||||
}, [JSON.stringify(defaultValue), multiSelect]);
|
}, [JSON.stringify(defaultValue), multiSelect]);
|
||||||
|
|
||||||
const groupby = formData?.groupby?.[0]?.length
|
const groupbys = ensureIsArray(formData.groupby).map(getColumnLabel);
|
||||||
? formData?.groupby?.[0]
|
const groupby = groupbys[0].length ? groupbys[0] : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
const withData = groupby
|
const withData = groupby
|
||||||
? data.filter(dataItem =>
|
? data.filter(row => groupby.includes(row.column_name as string))
|
||||||
// @ts-ignore
|
|
||||||
groupby.includes(dataItem.column_name),
|
|
||||||
)
|
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
const columns = data ? withData : [];
|
const columns = data ? withData : [];
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
ensureIsArray,
|
||||||
|
getColumnLabel,
|
||||||
getNumberFormatter,
|
getNumberFormatter,
|
||||||
NumberFormats,
|
NumberFormats,
|
||||||
styled,
|
styled,
|
||||||
|
@ -119,7 +121,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { min, max }: { min: number; max: number } = row;
|
const { min, max }: { min: number; max: number } = row;
|
||||||
const { groupby, defaultValue, inputRef } = formData;
|
const { groupby, defaultValue, inputRef } = formData;
|
||||||
const [col = ''] = groupby || [];
|
const [col = ''] = ensureIsArray(groupby).map(getColumnLabel);
|
||||||
const [value, setValue] = useState<[number, number]>(
|
const [value, setValue] = useState<[number, number]>(
|
||||||
defaultValue ?? [min, max],
|
defaultValue ?? [min, max],
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
ExtraFormData,
|
ExtraFormData,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
|
getColumnLabel,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
smartDateDetailedFormatter,
|
smartDateDetailedFormatter,
|
||||||
t,
|
t,
|
||||||
|
@ -94,7 +95,10 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||||
defaultToFirstItem,
|
defaultToFirstItem,
|
||||||
searchAllOptions,
|
searchAllOptions,
|
||||||
} = formData;
|
} = formData;
|
||||||
const groupby = ensureIsArray<string>(formData.groupby);
|
const groupby = useMemo(
|
||||||
|
() => ensureIsArray(formData.groupby).map(getColumnLabel),
|
||||||
|
[formData.groupby],
|
||||||
|
);
|
||||||
const [col] = groupby;
|
const [col] = groupby;
|
||||||
const [initialColtypeMap] = useState(coltypeMap);
|
const [initialColtypeMap] = useState(coltypeMap);
|
||||||
const [dataMask, dispatchDataMask] = useImmerReducer(reducer, {
|
const [dataMask, dispatchDataMask] = useImmerReducer(reducer, {
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
import {
|
import {
|
||||||
buildQueryContext,
|
buildQueryContext,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
|
getColumnLabel,
|
||||||
|
isPhysicalColumn,
|
||||||
QueryObject,
|
QueryObject,
|
||||||
QueryObjectFilterClause,
|
QueryObjectFilterClause,
|
||||||
BuildQuery,
|
BuildQuery,
|
||||||
|
@ -35,15 +37,16 @@ const buildQuery: BuildQuery<PluginFilterSelectQueryFormData> = (
|
||||||
const { columns = [], filters = [] } = baseQueryObject;
|
const { columns = [], filters = [] } = baseQueryObject;
|
||||||
const extraFilters: QueryObjectFilterClause[] = [];
|
const extraFilters: QueryObjectFilterClause[] = [];
|
||||||
if (search) {
|
if (search) {
|
||||||
columns.forEach(column => {
|
columns.filter(isPhysicalColumn).forEach(column => {
|
||||||
if (coltypeMap[column] === GenericDataType.STRING) {
|
const label = getColumnLabel(column);
|
||||||
|
if (coltypeMap[label] === GenericDataType.STRING) {
|
||||||
extraFilters.push({
|
extraFilters.push({
|
||||||
col: column,
|
col: column,
|
||||||
op: 'ILIKE',
|
op: 'ILIKE',
|
||||||
val: `%${search}%`,
|
val: `%${search}%`,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
coltypeMap[column] === GenericDataType.NUMERIC &&
|
coltypeMap[label] === GenericDataType.NUMERIC &&
|
||||||
!Number.isNaN(Number(search))
|
!Number.isNaN(Number(search))
|
||||||
) {
|
) {
|
||||||
// for numeric columns we apply a >= where clause
|
// for numeric columns we apply a >= where clause
|
||||||
|
|
|
@ -582,6 +582,7 @@ class ChartDataBoxplotOptionsSchema(ChartDataPostProcessingOperationOptionsSchem
|
||||||
"references to datasource metrics (strings), or ad-hoc metrics"
|
"references to datasource metrics (strings), or ad-hoc metrics"
|
||||||
"which are defined only within the query object. See "
|
"which are defined only within the query object. See "
|
||||||
"`ChartDataAdhocMetricSchema` for the structure of ad-hoc metrics.",
|
"`ChartDataAdhocMetricSchema` for the structure of ad-hoc metrics.",
|
||||||
|
allow_none=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
whisker_type = fields.String(
|
whisker_type = fields.String(
|
||||||
|
@ -771,8 +772,11 @@ class ChartDataPostProcessingOperationSchema(Schema):
|
||||||
|
|
||||||
|
|
||||||
class ChartDataFilterSchema(Schema):
|
class ChartDataFilterSchema(Schema):
|
||||||
col = fields.String(
|
col = fields.Raw(
|
||||||
description="The column to filter.", required=True, example="country"
|
description="The column to filter by. Can be either a string (physical or "
|
||||||
|
"saved expression) or an object (adhoc column)",
|
||||||
|
required=True,
|
||||||
|
example="country",
|
||||||
)
|
)
|
||||||
op = fields.String( # pylint: disable=invalid-name
|
op = fields.String( # pylint: disable=invalid-name
|
||||||
description="The comparison operator.",
|
description="The comparison operator.",
|
||||||
|
@ -961,7 +965,7 @@ class ChartDataQueryObjectSchema(Schema):
|
||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
groupby = fields.List(
|
groupby = fields.List(
|
||||||
fields.String(),
|
fields.Raw(),
|
||||||
description="Columns by which to group the query. "
|
description="Columns by which to group the query. "
|
||||||
"This field is deprecated, use `columns` instead.",
|
"This field is deprecated, use `columns` instead.",
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
|
@ -1012,7 +1016,7 @@ class ChartDataQueryObjectSchema(Schema):
|
||||||
description="Is the `query_object` a timeseries.", allow_none=True,
|
description="Is the `query_object` a timeseries.", allow_none=True,
|
||||||
)
|
)
|
||||||
series_columns = fields.List(
|
series_columns = fields.List(
|
||||||
fields.String(),
|
fields.Raw(),
|
||||||
description="Columns to use when limiting series count. "
|
description="Columns to use when limiting series count. "
|
||||||
"All columns must be present in the `columns` property. "
|
"All columns must be present in the `columns` property. "
|
||||||
"Requires `series_limit` and `series_limit_metric` to be set.",
|
"Requires `series_limit` and `series_limit_metric` to be set.",
|
||||||
|
@ -1062,7 +1066,7 @@ class ChartDataQueryObjectSchema(Schema):
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
)
|
)
|
||||||
columns = fields.List(
|
columns = fields.List(
|
||||||
fields.String(),
|
fields.Raw(),
|
||||||
description="Columns which to select in the query.",
|
description="Columns which to select in the query.",
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,7 +28,9 @@ from superset.utils.core import (
|
||||||
extract_column_dtype,
|
extract_column_dtype,
|
||||||
extract_dataframe_dtypes,
|
extract_dataframe_dtypes,
|
||||||
ExtraFiltersReasonType,
|
ExtraFiltersReasonType,
|
||||||
|
get_column_name,
|
||||||
get_time_filter_status,
|
get_time_filter_status,
|
||||||
|
is_adhoc_column,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -114,14 +116,16 @@ def _get_full(
|
||||||
datasource, query_obj.applied_time_extras
|
datasource, query_obj.applied_time_extras
|
||||||
)
|
)
|
||||||
payload["applied_filters"] = [
|
payload["applied_filters"] = [
|
||||||
{"column": col}
|
{"column": get_column_name(col)}
|
||||||
for col in filter_columns
|
for col in filter_columns
|
||||||
if col in columns or col in applied_template_filters
|
if is_adhoc_column(col) or col in columns or col in applied_template_filters
|
||||||
] + applied_time_columns
|
] + applied_time_columns
|
||||||
payload["rejected_filters"] = [
|
payload["rejected_filters"] = [
|
||||||
{"reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, "column": col}
|
{"reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, "column": col}
|
||||||
for col in filter_columns
|
for col in filter_columns
|
||||||
if col not in columns and col not in applied_template_filters
|
if not is_adhoc_column(col)
|
||||||
|
and col not in columns
|
||||||
|
and col not in applied_template_filters
|
||||||
] + rejected_time_columns
|
] + rejected_time_columns
|
||||||
|
|
||||||
if result_type == ChartDataResultType.RESULTS and status != QueryStatus.FAILED:
|
if result_type == ChartDataResultType.RESULTS and status != QueryStatus.FAILED:
|
||||||
|
|
|
@ -46,6 +46,7 @@ from superset.utils.core import (
|
||||||
DatasourceDict,
|
DatasourceDict,
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
error_msg_from_exception,
|
error_msg_from_exception,
|
||||||
|
get_column_names_from_columns,
|
||||||
get_column_names_from_metrics,
|
get_column_names_from_metrics,
|
||||||
get_metric_names,
|
get_metric_names,
|
||||||
normalize_dttm_col,
|
normalize_dttm_col,
|
||||||
|
@ -453,7 +454,7 @@ class QueryContext:
|
||||||
try:
|
try:
|
||||||
invalid_columns = [
|
invalid_columns = [
|
||||||
col
|
col
|
||||||
for col in query_obj.columns
|
for col in get_column_names_from_columns(query_obj.columns)
|
||||||
+ get_column_names_from_metrics(query_obj.metrics or [])
|
+ get_column_names_from_metrics(query_obj.metrics or [])
|
||||||
if col not in self.datasource.column_names and col != DTTM_ALIAS
|
if col not in self.datasource.column_names and col != DTTM_ALIAS
|
||||||
]
|
]
|
||||||
|
|
|
@ -27,13 +27,14 @@ from superset.common.chart_data import ChartDataResultType
|
||||||
from superset.connectors.base.models import BaseDatasource
|
from superset.connectors.base.models import BaseDatasource
|
||||||
from superset.connectors.connector_registry import ConnectorRegistry
|
from superset.connectors.connector_registry import ConnectorRegistry
|
||||||
from superset.exceptions import QueryObjectValidationError
|
from superset.exceptions import QueryObjectValidationError
|
||||||
from superset.typing import Metric, OrderBy
|
from superset.typing import Column, Metric, OrderBy
|
||||||
from superset.utils import pandas_postprocessing
|
from superset.utils import pandas_postprocessing
|
||||||
from superset.utils.core import (
|
from superset.utils.core import (
|
||||||
apply_max_row_limit,
|
apply_max_row_limit,
|
||||||
DatasourceDict,
|
DatasourceDict,
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
find_duplicates,
|
find_duplicates,
|
||||||
|
get_column_names,
|
||||||
get_metric_names,
|
get_metric_names,
|
||||||
is_adhoc_metric,
|
is_adhoc_metric,
|
||||||
json_int_dttm_ser,
|
json_int_dttm_ser,
|
||||||
|
@ -83,7 +84,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||||
annotation_layers: List[Dict[str, Any]]
|
annotation_layers: List[Dict[str, Any]]
|
||||||
applied_time_extras: Dict[str, str]
|
applied_time_extras: Dict[str, str]
|
||||||
apply_fetch_values_predicate: bool
|
apply_fetch_values_predicate: bool
|
||||||
columns: List[str]
|
columns: List[Column]
|
||||||
datasource: Optional[BaseDatasource]
|
datasource: Optional[BaseDatasource]
|
||||||
extras: Dict[str, Any]
|
extras: Dict[str, Any]
|
||||||
filter: List[QueryObjectFilterClause]
|
filter: List[QueryObjectFilterClause]
|
||||||
|
@ -93,19 +94,19 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||||
inner_to_dttm: Optional[datetime]
|
inner_to_dttm: Optional[datetime]
|
||||||
is_rowcount: bool
|
is_rowcount: bool
|
||||||
is_timeseries: bool
|
is_timeseries: bool
|
||||||
|
metrics: Optional[List[Metric]]
|
||||||
order_desc: bool
|
order_desc: bool
|
||||||
orderby: List[OrderBy]
|
orderby: List[OrderBy]
|
||||||
metrics: Optional[List[Metric]]
|
post_processing: List[Dict[str, Any]]
|
||||||
result_type: Optional[ChartDataResultType]
|
result_type: Optional[ChartDataResultType]
|
||||||
row_limit: int
|
row_limit: int
|
||||||
row_offset: int
|
row_offset: int
|
||||||
series_columns: List[str]
|
series_columns: List[Column]
|
||||||
series_limit: int
|
series_limit: int
|
||||||
series_limit_metric: Optional[Metric]
|
series_limit_metric: Optional[Metric]
|
||||||
time_offsets: List[str]
|
time_offsets: List[str]
|
||||||
time_shift: Optional[timedelta]
|
time_shift: Optional[timedelta]
|
||||||
to_dttm: Optional[datetime]
|
to_dttm: Optional[datetime]
|
||||||
post_processing: List[Dict[str, Any]]
|
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
||||||
self,
|
self,
|
||||||
|
@ -113,7 +114,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||||
annotation_layers: Optional[List[Dict[str, Any]]] = None,
|
annotation_layers: Optional[List[Dict[str, Any]]] = None,
|
||||||
applied_time_extras: Optional[Dict[str, str]] = None,
|
applied_time_extras: Optional[Dict[str, str]] = None,
|
||||||
apply_fetch_values_predicate: bool = False,
|
apply_fetch_values_predicate: bool = False,
|
||||||
columns: Optional[List[str]] = None,
|
columns: Optional[List[Column]] = None,
|
||||||
datasource: Optional[DatasourceDict] = None,
|
datasource: Optional[DatasourceDict] = None,
|
||||||
extras: Optional[Dict[str, Any]] = None,
|
extras: Optional[Dict[str, Any]] = None,
|
||||||
filters: Optional[List[QueryObjectFilterClause]] = None,
|
filters: Optional[List[QueryObjectFilterClause]] = None,
|
||||||
|
@ -127,7 +128,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||||
result_type: Optional[ChartDataResultType] = None,
|
result_type: Optional[ChartDataResultType] = None,
|
||||||
row_limit: Optional[int] = None,
|
row_limit: Optional[int] = None,
|
||||||
row_offset: Optional[int] = None,
|
row_offset: Optional[int] = None,
|
||||||
series_columns: Optional[List[str]] = None,
|
series_columns: Optional[List[Column]] = None,
|
||||||
series_limit: int = 0,
|
series_limit: int = 0,
|
||||||
series_limit_metric: Optional[Metric] = None,
|
series_limit_metric: Optional[Metric] = None,
|
||||||
time_range: Optional[str] = None,
|
time_range: Optional[str] = None,
|
||||||
|
@ -266,9 +267,9 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def column_names(self) -> List[str]:
|
def column_names(self) -> List[str]:
|
||||||
"""Return column names (labels). Reserved for future adhoc calculated
|
"""Return column names (labels). Gives priority to groupbys if both groupbys
|
||||||
columns."""
|
and metrics are non-empty, otherwise returns column labels."""
|
||||||
return self.columns
|
return get_column_names(self.columns)
|
||||||
|
|
||||||
def validate(
|
def validate(
|
||||||
self, raise_exceptions: Optional[bool] = True
|
self, raise_exceptions: Optional[bool] = True
|
||||||
|
|
|
@ -324,7 +324,7 @@ class BaseDatasource(
|
||||||
if query_context:
|
if query_context:
|
||||||
column_names.update(
|
column_names.update(
|
||||||
[
|
[
|
||||||
column
|
utils.get_column_name(column)
|
||||||
for query in query_context.queries
|
for query in query_context.queries
|
||||||
for column in query.columns
|
for column in query.columns
|
||||||
]
|
]
|
||||||
|
|
|
@ -88,10 +88,12 @@ from superset.models.annotations import Annotation
|
||||||
from superset.models.core import Database
|
from superset.models.core import Database
|
||||||
from superset.models.helpers import AuditMixinNullable, CertificationMixin, QueryResult
|
from superset.models.helpers import AuditMixinNullable, CertificationMixin, QueryResult
|
||||||
from superset.sql_parse import ParsedQuery
|
from superset.sql_parse import ParsedQuery
|
||||||
from superset.typing import AdhocMetric, Metric, OrderBy, QueryObjectDict
|
from superset.typing import AdhocColumn, AdhocMetric, Metric, OrderBy, QueryObjectDict
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
from superset.utils.core import (
|
from superset.utils.core import (
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
|
get_column_name,
|
||||||
|
is_adhoc_column,
|
||||||
QueryObjectFilterClause,
|
QueryObjectFilterClause,
|
||||||
remove_duplicates,
|
remove_duplicates,
|
||||||
)
|
)
|
||||||
|
@ -877,6 +879,26 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
|
|
||||||
return self.make_sqla_column_compatible(sqla_metric, label)
|
return self.make_sqla_column_compatible(sqla_metric, label)
|
||||||
|
|
||||||
|
def adhoc_column_to_sqla(
|
||||||
|
self,
|
||||||
|
col: AdhocColumn,
|
||||||
|
template_processor: Optional[BaseTemplateProcessor] = None,
|
||||||
|
) -> ColumnElement:
|
||||||
|
"""
|
||||||
|
Turn an adhoc column into a sqlalchemy column.
|
||||||
|
|
||||||
|
:param col: Adhoc column definition
|
||||||
|
:param template_processor: template_processor instance
|
||||||
|
:returns: The metric defined as a sqlalchemy column
|
||||||
|
:rtype: sqlalchemy.sql.column
|
||||||
|
"""
|
||||||
|
label = utils.get_column_name(col)
|
||||||
|
expression = col["sqlExpression"]
|
||||||
|
if template_processor and expression:
|
||||||
|
expression = template_processor.process_template(expression)
|
||||||
|
sqla_metric = literal_column(expression)
|
||||||
|
return self.make_sqla_column_compatible(sqla_metric, label)
|
||||||
|
|
||||||
def make_sqla_column_compatible(
|
def make_sqla_column_compatible(
|
||||||
self, sqla_col: ColumnElement, label: Optional[str] = None
|
self, sqla_col: ColumnElement, label: Optional[str] = None
|
||||||
) -> ColumnElement:
|
) -> ColumnElement:
|
||||||
|
@ -951,14 +973,14 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
|
def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
|
||||||
self,
|
self,
|
||||||
apply_fetch_values_predicate: bool = False,
|
apply_fetch_values_predicate: bool = False,
|
||||||
columns: Optional[List[str]] = None,
|
columns: Optional[List[Column]] = None,
|
||||||
extras: Optional[Dict[str, Any]] = None,
|
extras: Optional[Dict[str, Any]] = None,
|
||||||
filter: Optional[ # pylint: disable=redefined-builtin
|
filter: Optional[ # pylint: disable=redefined-builtin
|
||||||
List[QueryObjectFilterClause]
|
List[QueryObjectFilterClause]
|
||||||
] = None,
|
] = None,
|
||||||
from_dttm: Optional[datetime] = None,
|
from_dttm: Optional[datetime] = None,
|
||||||
granularity: Optional[str] = None,
|
granularity: Optional[str] = None,
|
||||||
groupby: Optional[List[str]] = None,
|
groupby: Optional[List[Column]] = None,
|
||||||
inner_from_dttm: Optional[datetime] = None,
|
inner_from_dttm: Optional[datetime] = None,
|
||||||
inner_to_dttm: Optional[datetime] = None,
|
inner_to_dttm: Optional[datetime] = None,
|
||||||
is_rowcount: bool = False,
|
is_rowcount: bool = False,
|
||||||
|
@ -967,7 +989,7 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
orderby: Optional[List[OrderBy]] = None,
|
orderby: Optional[List[OrderBy]] = None,
|
||||||
order_desc: bool = True,
|
order_desc: bool = True,
|
||||||
to_dttm: Optional[datetime] = None,
|
to_dttm: Optional[datetime] = None,
|
||||||
series_columns: Optional[List[str]] = None,
|
series_columns: Optional[List[Column]] = None,
|
||||||
series_limit: Optional[int] = None,
|
series_limit: Optional[int] = None,
|
||||||
series_limit_metric: Optional[Metric] = None,
|
series_limit_metric: Optional[Metric] = None,
|
||||||
row_limit: Optional[int] = None,
|
row_limit: Optional[int] = None,
|
||||||
|
@ -995,7 +1017,9 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
"table_columns": [col.column_name for col in self.columns],
|
"table_columns": [col.column_name for col in self.columns],
|
||||||
"filter": filter,
|
"filter": filter,
|
||||||
}
|
}
|
||||||
series_columns = series_columns or []
|
columns = columns or []
|
||||||
|
groupby = groupby or []
|
||||||
|
series_column_names = utils.get_column_names(series_columns or [])
|
||||||
# deprecated, to be removed in 2.0
|
# deprecated, to be removed in 2.0
|
||||||
if is_timeseries and timeseries_limit:
|
if is_timeseries and timeseries_limit:
|
||||||
series_limit = timeseries_limit
|
series_limit = timeseries_limit
|
||||||
|
@ -1021,6 +1045,7 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
columns_by_name: Dict[str, TableColumn] = {
|
columns_by_name: Dict[str, TableColumn] = {
|
||||||
col.column_name: col for col in self.columns
|
col.column_name: col for col in self.columns
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics_by_name: Dict[str, SqlMetric] = {m.metric_name: m for m in self.metrics}
|
metrics_by_name: Dict[str, SqlMetric] = {m.metric_name: m for m in self.metrics}
|
||||||
|
|
||||||
if not granularity and is_timeseries:
|
if not granularity and is_timeseries:
|
||||||
|
@ -1092,7 +1117,6 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
groupby_series_columns = {}
|
groupby_series_columns = {}
|
||||||
|
|
||||||
# filter out the pseudo column __timestamp from columns
|
# filter out the pseudo column __timestamp from columns
|
||||||
columns = columns or []
|
|
||||||
columns = [col for col in columns if col != utils.DTTM_ALIAS]
|
columns = [col for col in columns if col != utils.DTTM_ALIAS]
|
||||||
dttm_col = columns_by_name.get(granularity) if granularity else None
|
dttm_col = columns_by_name.get(granularity) if granularity else None
|
||||||
|
|
||||||
|
@ -1100,22 +1124,27 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
# dedup columns while preserving order
|
# dedup columns while preserving order
|
||||||
columns = groupby or columns
|
columns = groupby or columns
|
||||||
for selected in columns:
|
for selected in columns:
|
||||||
# if groupby field/expr equals granularity field/expr
|
if isinstance(selected, str):
|
||||||
if selected == granularity:
|
# if groupby field/expr equals granularity field/expr
|
||||||
sqla_col = columns_by_name[selected]
|
if selected == granularity:
|
||||||
outer = sqla_col.get_timestamp_expression(
|
table_col = columns_by_name[selected]
|
||||||
time_grain=time_grain,
|
outer = table_col.get_timestamp_expression(
|
||||||
label=selected,
|
time_grain=time_grain,
|
||||||
template_processor=template_processor,
|
label=selected,
|
||||||
)
|
template_processor=template_processor,
|
||||||
# if groupby field equals a selected column
|
)
|
||||||
elif selected in columns_by_name:
|
# if groupby field equals a selected column
|
||||||
outer = columns_by_name[selected].get_sqla_col()
|
elif selected in columns_by_name:
|
||||||
|
outer = columns_by_name[selected].get_sqla_col()
|
||||||
|
else:
|
||||||
|
outer = literal_column(f"({selected})")
|
||||||
|
outer = self.make_sqla_column_compatible(outer, selected)
|
||||||
else:
|
else:
|
||||||
outer = literal_column(f"({selected})")
|
outer = self.adhoc_column_to_sqla(
|
||||||
outer = self.make_sqla_column_compatible(outer, selected)
|
col=selected, template_processor=template_processor
|
||||||
|
)
|
||||||
groupby_all_columns[outer.name] = outer
|
groupby_all_columns[outer.name] = outer
|
||||||
if not series_columns or outer.name in series_columns:
|
if not series_column_names or outer.name in series_column_names:
|
||||||
groupby_series_columns[outer.name] = outer
|
groupby_series_columns[outer.name] = outer
|
||||||
select_exprs.append(outer)
|
select_exprs.append(outer)
|
||||||
elif columns:
|
elif columns:
|
||||||
|
@ -1190,29 +1219,36 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
for flt in filter: # type: ignore
|
for flt in filter: # type: ignore
|
||||||
if not all(flt.get(s) for s in ["col", "op"]):
|
if not all(flt.get(s) for s in ["col", "op"]):
|
||||||
continue
|
continue
|
||||||
col = flt["col"]
|
flt_col = flt["col"]
|
||||||
val = flt.get("val")
|
val = flt.get("val")
|
||||||
op = flt["op"].upper()
|
op = flt["op"].upper()
|
||||||
col_obj = (
|
col_obj: Optional[TableColumn] = None
|
||||||
dttm_col
|
sqla_col: Optional[Column] = None
|
||||||
if col == utils.DTTM_ALIAS and is_timeseries and dttm_col
|
if flt_col == utils.DTTM_ALIAS and is_timeseries and dttm_col:
|
||||||
else columns_by_name.get(col)
|
col_obj = dttm_col
|
||||||
)
|
elif is_adhoc_column(flt_col):
|
||||||
|
sqla_col = self.adhoc_column_to_sqla(flt_col)
|
||||||
|
else:
|
||||||
|
col_obj = columns_by_name.get(flt_col)
|
||||||
filter_grain = flt.get("grain")
|
filter_grain = flt.get("grain")
|
||||||
|
|
||||||
if is_feature_enabled("ENABLE_TEMPLATE_REMOVE_FILTERS"):
|
if is_feature_enabled("ENABLE_TEMPLATE_REMOVE_FILTERS"):
|
||||||
if col in removed_filters:
|
if get_column_name(flt_col) in removed_filters:
|
||||||
# Skip generating SQLA filter when the jinja template handles it.
|
# Skip generating SQLA filter when the jinja template handles it.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if col_obj:
|
if col_obj or sqla_col is not None:
|
||||||
if filter_grain:
|
if sqla_col is not None:
|
||||||
|
pass
|
||||||
|
elif col_obj and filter_grain:
|
||||||
sqla_col = col_obj.get_timestamp_expression(
|
sqla_col = col_obj.get_timestamp_expression(
|
||||||
time_grain=filter_grain, template_processor=template_processor
|
time_grain=filter_grain, template_processor=template_processor
|
||||||
)
|
)
|
||||||
else:
|
elif col_obj:
|
||||||
sqla_col = col_obj.get_sqla_col()
|
sqla_col = col_obj.get_sqla_col()
|
||||||
col_spec = db_engine_spec.get_column_spec(col_obj.type)
|
col_spec = db_engine_spec.get_column_spec(
|
||||||
|
col_obj.type if col_obj else None
|
||||||
|
)
|
||||||
is_list_target = op in (
|
is_list_target = op in (
|
||||||
utils.FilterOperator.IN.value,
|
utils.FilterOperator.IN.value,
|
||||||
utils.FilterOperator.NOT_IN.value,
|
utils.FilterOperator.NOT_IN.value,
|
||||||
|
@ -1348,6 +1384,7 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
||||||
inner_groupby_exprs = []
|
inner_groupby_exprs = []
|
||||||
inner_select_exprs = []
|
inner_select_exprs = []
|
||||||
for gby_name, gby_obj in groupby_series_columns.items():
|
for gby_name, gby_obj in groupby_series_columns.items():
|
||||||
|
label = get_column_name(gby_name)
|
||||||
inner = self.make_sqla_column_compatible(gby_obj, gby_name + "__")
|
inner = self.make_sqla_column_compatible(gby_obj, gby_name + "__")
|
||||||
inner_groupby_exprs.append(inner)
|
inner_groupby_exprs.append(inner)
|
||||||
inner_select_exprs.append(inner)
|
inner_select_exprs.append(inner)
|
||||||
|
|
|
@ -58,6 +58,13 @@ class AdhocMetric(TypedDict, total=False):
|
||||||
aggregate: str
|
aggregate: str
|
||||||
column: Optional[AdhocMetricColumn]
|
column: Optional[AdhocMetricColumn]
|
||||||
expressionType: Literal["SIMPLE", "SQL"]
|
expressionType: Literal["SIMPLE", "SQL"]
|
||||||
|
hasCustomLabel: Optional[bool]
|
||||||
|
label: Optional[str]
|
||||||
|
sqlExpression: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class AdhocColumn(TypedDict, total=False):
|
||||||
|
hasCustomLabel: Optional[bool]
|
||||||
label: Optional[str]
|
label: Optional[str]
|
||||||
sqlExpression: Optional[str]
|
sqlExpression: Optional[str]
|
||||||
|
|
||||||
|
@ -72,6 +79,7 @@ FilterValue = Union[bool, datetime, float, int, str]
|
||||||
FilterValues = Union[FilterValue, List[FilterValue], Tuple[FilterValue]]
|
FilterValues = Union[FilterValue, List[FilterValue], Tuple[FilterValue]]
|
||||||
FormData = Dict[str, Any]
|
FormData = Dict[str, Any]
|
||||||
Granularity = Union[str, Dict[str, Union[str, float]]]
|
Granularity = Union[str, Dict[str, Union[str, float]]]
|
||||||
|
Column = Union[AdhocColumn, str]
|
||||||
Metric = Union[AdhocMetric, str]
|
Metric = Union[AdhocMetric, str]
|
||||||
OrderBy = Tuple[Metric, bool]
|
OrderBy = Tuple[Metric, bool]
|
||||||
QueryObjectDict = Dict[str, Any]
|
QueryObjectDict = Dict[str, Any]
|
||||||
|
|
|
@ -98,8 +98,10 @@ from superset.exceptions import (
|
||||||
SupersetTimeoutException,
|
SupersetTimeoutException,
|
||||||
)
|
)
|
||||||
from superset.typing import (
|
from superset.typing import (
|
||||||
|
AdhocColumn,
|
||||||
AdhocMetric,
|
AdhocMetric,
|
||||||
AdhocMetricColumn,
|
AdhocMetricColumn,
|
||||||
|
Column,
|
||||||
FilterValues,
|
FilterValues,
|
||||||
FlaskResponse,
|
FlaskResponse,
|
||||||
FormData,
|
FormData,
|
||||||
|
@ -1265,6 +1267,29 @@ def is_adhoc_metric(metric: Metric) -> TypeGuard[AdhocMetric]:
|
||||||
return isinstance(metric, dict) and "expressionType" in metric
|
return isinstance(metric, dict) and "expressionType" in metric
|
||||||
|
|
||||||
|
|
||||||
|
def is_adhoc_column(column: Column) -> TypeGuard[AdhocColumn]:
|
||||||
|
return isinstance(column, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def get_column_name(column: Column) -> str:
|
||||||
|
"""
|
||||||
|
Extract label from column
|
||||||
|
|
||||||
|
:param column: object to extract label from
|
||||||
|
:return: String representation of column
|
||||||
|
:raises ValueError: if metric object is invalid
|
||||||
|
"""
|
||||||
|
if isinstance(column, dict):
|
||||||
|
label = column.get("label")
|
||||||
|
if label:
|
||||||
|
return label
|
||||||
|
expr = column.get("sqlExpression")
|
||||||
|
if expr:
|
||||||
|
return expr
|
||||||
|
raise Exception("Missing label")
|
||||||
|
return column
|
||||||
|
|
||||||
|
|
||||||
def get_metric_name(metric: Metric) -> str:
|
def get_metric_name(metric: Metric) -> str:
|
||||||
"""
|
"""
|
||||||
Extract label from metric
|
Extract label from metric
|
||||||
|
@ -1294,11 +1319,15 @@ def get_metric_name(metric: Metric) -> str:
|
||||||
return metric # type: ignore
|
return metric # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def get_metric_names(metrics: Sequence[Metric]) -> List[str]:
|
def get_column_names(columns: Optional[Sequence[Column]]) -> List[str]:
|
||||||
return [metric for metric in map(get_metric_name, metrics) if metric]
|
return [column for column in map(get_column_name, columns or []) if column]
|
||||||
|
|
||||||
|
|
||||||
def get_first_metric_name(metrics: Sequence[Metric]) -> Optional[str]:
|
def get_metric_names(metrics: Optional[Sequence[Metric]]) -> List[str]:
|
||||||
|
return [metric for metric in map(get_metric_name, metrics or []) if metric]
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_metric_name(metrics: Optional[Sequence[Metric]]) -> Optional[str]:
|
||||||
metric_labels = get_metric_names(metrics)
|
metric_labels = get_metric_names(metrics)
|
||||||
return metric_labels[0] if metric_labels else None
|
return metric_labels[0] if metric_labels else None
|
||||||
|
|
||||||
|
@ -1518,6 +1547,30 @@ def get_form_data_token(form_data: Dict[str, Any]) -> str:
|
||||||
return form_data.get("token") or "token_" + uuid.uuid4().hex[:8]
|
return form_data.get("token") or "token_" + uuid.uuid4().hex[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def get_column_name_from_column(column: Column) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Extract the physical column that a column is referencing. If the column is
|
||||||
|
an adhoc column, always returns `None`.
|
||||||
|
|
||||||
|
:param column: Physical and ad-hoc column
|
||||||
|
:return: column name if physical column, otherwise None
|
||||||
|
"""
|
||||||
|
if is_adhoc_column(column):
|
||||||
|
return None
|
||||||
|
return column # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_column_names_from_columns(columns: List[Column]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extract the physical columns that a list of columns are referencing. Ignore
|
||||||
|
adhoc columns
|
||||||
|
|
||||||
|
:param columns: Physical and adhoc columns
|
||||||
|
:return: column names of all physical columns
|
||||||
|
"""
|
||||||
|
return [col for col in map(get_column_name_from_column, columns) if col]
|
||||||
|
|
||||||
|
|
||||||
def get_column_name_from_metric(metric: Metric) -> Optional[str]:
|
def get_column_name_from_metric(metric: Metric) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Extract the column that a metric is referencing. If the metric isn't
|
Extract the column that a metric is referencing. If the metric isn't
|
||||||
|
|
127
superset/viz.py
127
superset/viz.py
|
@ -66,13 +66,18 @@ from superset.exceptions import (
|
||||||
)
|
)
|
||||||
from superset.extensions import cache_manager, security_manager
|
from superset.extensions import cache_manager, security_manager
|
||||||
from superset.models.helpers import QueryResult
|
from superset.models.helpers import QueryResult
|
||||||
from superset.typing import Metric, QueryObjectDict, VizData, VizPayload
|
from superset.typing import Column, Metric, QueryObjectDict, VizData, VizPayload
|
||||||
from superset.utils import core as utils, csv
|
from superset.utils import core as utils, csv
|
||||||
from superset.utils.cache import set_and_log_cache
|
from superset.utils.cache import set_and_log_cache
|
||||||
from superset.utils.core import (
|
from superset.utils.core import (
|
||||||
apply_max_row_limit,
|
apply_max_row_limit,
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
ExtraFiltersReasonType,
|
ExtraFiltersReasonType,
|
||||||
|
get_column_name,
|
||||||
|
get_column_names,
|
||||||
|
get_column_names_from_columns,
|
||||||
|
get_metric_names,
|
||||||
|
is_adhoc_column,
|
||||||
JS_MAX_INTEGER,
|
JS_MAX_INTEGER,
|
||||||
merge_extra_filters,
|
merge_extra_filters,
|
||||||
QueryMode,
|
QueryMode,
|
||||||
|
@ -132,7 +137,7 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||||
self.query = ""
|
self.query = ""
|
||||||
self.token = utils.get_form_data_token(form_data)
|
self.token = utils.get_form_data_token(form_data)
|
||||||
|
|
||||||
self.groupby: List[str] = self.form_data.get("groupby") or []
|
self.groupby: List[Column] = self.form_data.get("groupby") or []
|
||||||
self.time_shift = timedelta()
|
self.time_shift = timedelta()
|
||||||
|
|
||||||
self.status: Optional[str] = None
|
self.status: Optional[str] = None
|
||||||
|
@ -303,19 +308,30 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||||
merge_extra_filters(self.form_data)
|
merge_extra_filters(self.form_data)
|
||||||
utils.split_adhoc_filters_into_base_filters(self.form_data)
|
utils.split_adhoc_filters_into_base_filters(self.form_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dedup_columns(*columns_args: Optional[List[Column]]) -> List[Column]:
|
||||||
|
# dedup groupby and columns while preserving order
|
||||||
|
labels: List[str] = []
|
||||||
|
deduped_columns: List[Column] = []
|
||||||
|
for columns in columns_args:
|
||||||
|
for column in columns or []:
|
||||||
|
label = get_column_name(column)
|
||||||
|
if label not in labels:
|
||||||
|
deduped_columns.append(column)
|
||||||
|
return deduped_columns
|
||||||
|
|
||||||
def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals
|
def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals
|
||||||
"""Building a query object"""
|
"""Building a query object"""
|
||||||
self.process_query_filters()
|
self.process_query_filters()
|
||||||
|
|
||||||
gb = self.groupby
|
|
||||||
metrics = self.all_metrics or []
|
metrics = self.all_metrics or []
|
||||||
columns = self.form_data.get("columns") or []
|
|
||||||
# merge list and dedup while preserving order
|
groupby = self.dedup_columns(self.groupby, self.form_data.get("columns"))
|
||||||
groupby = list(OrderedDict.fromkeys(gb + columns))
|
groupby_labels = get_column_names(groupby)
|
||||||
|
|
||||||
is_timeseries = self.is_timeseries
|
is_timeseries = self.is_timeseries
|
||||||
if DTTM_ALIAS in groupby:
|
if DTTM_ALIAS in groupby_labels:
|
||||||
groupby.remove(DTTM_ALIAS)
|
del groupby[groupby_labels.index(DTTM_ALIAS)]
|
||||||
is_timeseries = True
|
is_timeseries = True
|
||||||
|
|
||||||
granularity = self.form_data.get("granularity") or self.form_data.get(
|
granularity = self.form_data.get("granularity") or self.form_data.get(
|
||||||
|
@ -462,14 +478,16 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||||
self.datasource, applied_time_extras
|
self.datasource, applied_time_extras
|
||||||
)
|
)
|
||||||
payload["applied_filters"] = [
|
payload["applied_filters"] = [
|
||||||
{"column": col}
|
{"column": get_column_name(col)}
|
||||||
for col in filter_columns
|
for col in filter_columns
|
||||||
if col in columns or col in applied_template_filters
|
if is_adhoc_column(col) or col in columns or col in applied_template_filters
|
||||||
] + applied_time_columns
|
] + applied_time_columns
|
||||||
payload["rejected_filters"] = [
|
payload["rejected_filters"] = [
|
||||||
{"reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, "column": col}
|
{"reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, "column": col}
|
||||||
for col in filter_columns
|
for col in filter_columns
|
||||||
if col not in columns and col not in applied_template_filters
|
if not is_adhoc_column(col)
|
||||||
|
and col not in columns
|
||||||
|
and col not in applied_template_filters
|
||||||
] + rejected_time_columns
|
] + rejected_time_columns
|
||||||
if df is not None:
|
if df is not None:
|
||||||
payload["colnames"] = list(df.columns)
|
payload["colnames"] = list(df.columns)
|
||||||
|
@ -519,10 +537,12 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||||
try:
|
try:
|
||||||
invalid_columns = [
|
invalid_columns = [
|
||||||
col
|
col
|
||||||
for col in (query_obj.get("columns") or [])
|
for col in get_column_names_from_columns(
|
||||||
+ (query_obj.get("groupby") or [])
|
query_obj.get("columns") or []
|
||||||
|
)
|
||||||
|
+ get_column_names_from_columns(query_obj.get("groupby") or [])
|
||||||
+ utils.get_column_names_from_metrics(
|
+ utils.get_column_names_from_metrics(
|
||||||
cast(List[Metric], query_obj.get("metrics") or [],)
|
cast(List[Metric], query_obj.get("metrics") or [])
|
||||||
)
|
)
|
||||||
if col not in self.datasource.column_names
|
if col not in self.datasource.column_names
|
||||||
]
|
]
|
||||||
|
@ -688,16 +708,16 @@ class TableViz(BaseViz):
|
||||||
else QueryMode.AGGREGATE
|
else QueryMode.AGGREGATE
|
||||||
)
|
)
|
||||||
|
|
||||||
columns: List[str] = [] # output columns sans time and percent_metric column
|
columns: List[str] # output columns sans time and percent_metric column
|
||||||
percent_columns: List[str] = [] # percent columns that needs extra computation
|
percent_columns: List[str] = [] # percent columns that needs extra computation
|
||||||
|
|
||||||
if self.query_mode == QueryMode.RAW:
|
if self.query_mode == QueryMode.RAW:
|
||||||
columns = utils.get_metric_names(self.form_data.get("all_columns") or [])
|
columns = get_metric_names(self.form_data.get("all_columns"))
|
||||||
else:
|
else:
|
||||||
columns = utils.get_metric_names(
|
columns = get_column_names(self.groupby) + get_metric_names(
|
||||||
self.groupby + (self.form_data.get("metrics") or [])
|
self.form_data.get("metrics")
|
||||||
)
|
)
|
||||||
percent_columns = utils.get_metric_names(
|
percent_columns = get_metric_names(
|
||||||
self.form_data.get("percent_metrics") or []
|
self.form_data.get("percent_metrics") or []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -820,7 +840,7 @@ class TimeTableViz(BaseViz):
|
||||||
values: Union[List[str], str] = self.metric_labels
|
values: Union[List[str], str] = self.metric_labels
|
||||||
if self.form_data.get("groupby"):
|
if self.form_data.get("groupby"):
|
||||||
values = self.metric_labels[0]
|
values = self.metric_labels[0]
|
||||||
columns = self.form_data.get("groupby")
|
columns = get_column_names(self.form_data.get("groupby"))
|
||||||
pt = df.pivot_table(index=DTTM_ALIAS, columns=columns, values=values)
|
pt = df.pivot_table(index=DTTM_ALIAS, columns=columns, values=values)
|
||||||
pt.index = pt.index.map(str)
|
pt.index = pt.index.map(str)
|
||||||
pt = pt.sort_index()
|
pt = pt.sort_index()
|
||||||
|
@ -866,7 +886,9 @@ class PivotTableViz(BaseViz):
|
||||||
)
|
)
|
||||||
if not metrics:
|
if not metrics:
|
||||||
raise QueryObjectValidationError(_("Please choose at least one metric"))
|
raise QueryObjectValidationError(_("Please choose at least one metric"))
|
||||||
if set(groupby) & set(columns):
|
deduped_cols = self.dedup_columns(groupby, columns)
|
||||||
|
|
||||||
|
if len(deduped_cols) < (len(groupby) + len(columns)):
|
||||||
raise QueryObjectValidationError(_("Group By' and 'Columns' can't overlap"))
|
raise QueryObjectValidationError(_("Group By' and 'Columns' can't overlap"))
|
||||||
sort_by = self.form_data.get("timeseries_limit_metric")
|
sort_by = self.form_data.get("timeseries_limit_metric")
|
||||||
if sort_by:
|
if sort_by:
|
||||||
|
@ -931,18 +953,22 @@ class PivotTableViz(BaseViz):
|
||||||
groupby = self.form_data.get("groupby") or []
|
groupby = self.form_data.get("groupby") or []
|
||||||
columns = self.form_data.get("columns") or []
|
columns = self.form_data.get("columns") or []
|
||||||
|
|
||||||
for column_name in groupby + columns:
|
for column in groupby + columns:
|
||||||
column = self.datasource.get_column(column_name)
|
if is_adhoc_column(column):
|
||||||
if column and column.is_temporal:
|
# TODO: check data type
|
||||||
ts = df[column_name].apply(self._format_datetime)
|
pass
|
||||||
df[column_name] = ts
|
else:
|
||||||
|
column_obj = self.datasource.get_column(column)
|
||||||
|
if column_obj and column_obj.is_temporal:
|
||||||
|
ts = df[column].apply(self._format_datetime)
|
||||||
|
df[column] = ts
|
||||||
|
|
||||||
if self.form_data.get("transpose_pivot"):
|
if self.form_data.get("transpose_pivot"):
|
||||||
groupby, columns = columns, groupby
|
groupby, columns = columns, groupby
|
||||||
|
|
||||||
df = df.pivot_table(
|
df = df.pivot_table(
|
||||||
index=groupby,
|
index=get_column_names(groupby),
|
||||||
columns=columns,
|
columns=get_column_names(columns),
|
||||||
values=metrics,
|
values=metrics,
|
||||||
aggfunc=aggfuncs,
|
aggfunc=aggfuncs,
|
||||||
margins=self.form_data.get("pivot_margins"),
|
margins=self.form_data.get("pivot_margins"),
|
||||||
|
@ -1003,7 +1029,7 @@ class TreemapViz(BaseViz):
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
df = df.set_index(self.form_data.get("groupby"))
|
df = df.set_index(get_column_names(self.form_data.get("groupby")))
|
||||||
chart_data = [
|
chart_data = [
|
||||||
{"name": metric, "children": self._nest(metric, df)}
|
{"name": metric, "children": self._nest(metric, df)}
|
||||||
for metric in df.columns
|
for metric in df.columns
|
||||||
|
@ -1117,7 +1143,7 @@ class BubbleViz(NVD3Viz):
|
||||||
query_obj["groupby"].append(self.form_data.get("series"))
|
query_obj["groupby"].append(self.form_data.get("series"))
|
||||||
|
|
||||||
# dedup groupby if it happens to be the same
|
# dedup groupby if it happens to be the same
|
||||||
query_obj["groupby"] = list(dict.fromkeys(query_obj["groupby"]))
|
query_obj["groupby"] = self.dedup_columns(query_obj["groupby"])
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
self.x_metric = self.form_data["x"]
|
self.x_metric = self.form_data["x"]
|
||||||
|
@ -1142,7 +1168,7 @@ class BubbleViz(NVD3Viz):
|
||||||
df["y"] = df[[utils.get_metric_name(self.y_metric)]]
|
df["y"] = df[[utils.get_metric_name(self.y_metric)]]
|
||||||
df["size"] = df[[utils.get_metric_name(self.z_metric)]]
|
df["size"] = df[[utils.get_metric_name(self.z_metric)]]
|
||||||
df["shape"] = "circle"
|
df["shape"] = "circle"
|
||||||
df["group"] = df[[self.series]]
|
df["group"] = df[[get_column_name(self.series)]] # type: ignore
|
||||||
|
|
||||||
series: Dict[Any, List[Any]] = defaultdict(list)
|
series: Dict[Any, List[Any]] = defaultdict(list)
|
||||||
for row in df.to_dict(orient="records"):
|
for row in df.to_dict(orient="records"):
|
||||||
|
@ -1335,7 +1361,7 @@ class NVD3TimeSeriesViz(NVD3Viz):
|
||||||
if aggregate:
|
if aggregate:
|
||||||
df = df.pivot_table(
|
df = df.pivot_table(
|
||||||
index=DTTM_ALIAS,
|
index=DTTM_ALIAS,
|
||||||
columns=self.form_data.get("groupby"),
|
columns=get_column_names(self.form_data.get("groupby")),
|
||||||
values=self.metric_labels,
|
values=self.metric_labels,
|
||||||
fill_value=0,
|
fill_value=0,
|
||||||
aggfunc=sum,
|
aggfunc=sum,
|
||||||
|
@ -1343,7 +1369,7 @@ class NVD3TimeSeriesViz(NVD3Viz):
|
||||||
else:
|
else:
|
||||||
df = df.pivot_table(
|
df = df.pivot_table(
|
||||||
index=DTTM_ALIAS,
|
index=DTTM_ALIAS,
|
||||||
columns=self.form_data.get("groupby"),
|
columns=get_column_names(self.form_data.get("groupby")),
|
||||||
values=self.metric_labels,
|
values=self.metric_labels,
|
||||||
fill_value=self.pivot_fill_value,
|
fill_value=self.pivot_fill_value,
|
||||||
)
|
)
|
||||||
|
@ -1716,15 +1742,15 @@ class HistogramViz(BaseViz):
|
||||||
|
|
||||||
chart_data = []
|
chart_data = []
|
||||||
if len(self.groupby) > 0:
|
if len(self.groupby) > 0:
|
||||||
groups = df.groupby(self.groupby)
|
groups = df.groupby(get_column_names(self.groupby))
|
||||||
else:
|
else:
|
||||||
groups = [((), df)]
|
groups = [((), df)]
|
||||||
for keys, data in groups:
|
for keys, data in groups:
|
||||||
chart_data.extend(
|
chart_data.extend(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": self.labelify(keys, column),
|
"key": self.labelify(keys, get_column_name(column)),
|
||||||
"values": data[column].tolist(),
|
"values": data[get_column_name(column)].tolist(),
|
||||||
}
|
}
|
||||||
for column in self.columns
|
for column in self.columns
|
||||||
]
|
]
|
||||||
|
@ -1775,21 +1801,22 @@ class DistributionBarViz(BaseViz):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
metrics = self.metric_labels
|
metrics = self.metric_labels
|
||||||
columns = self.form_data.get("columns") or []
|
columns = get_column_names(self.form_data.get("columns"))
|
||||||
|
groupby = get_column_names(self.groupby)
|
||||||
|
|
||||||
# pandas will throw away nulls when grouping/pivoting,
|
# pandas will throw away nulls when grouping/pivoting,
|
||||||
# so we substitute NULL_STRING for any nulls in the necessary columns
|
# so we substitute NULL_STRING for any nulls in the necessary columns
|
||||||
filled_cols = self.groupby + columns
|
filled_cols = groupby + columns
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
df[filled_cols] = df[filled_cols].fillna(value=NULL_STRING)
|
df[filled_cols] = df[filled_cols].fillna(value=NULL_STRING)
|
||||||
|
|
||||||
sortby = utils.get_metric_name(
|
sortby = utils.get_metric_name(
|
||||||
self.form_data.get("timeseries_limit_metric") or metrics[0]
|
self.form_data.get("timeseries_limit_metric") or metrics[0]
|
||||||
)
|
)
|
||||||
row = df.groupby(self.groupby).sum()[sortby].copy()
|
row = df.groupby(groupby).sum()[sortby].copy()
|
||||||
is_asc = not self.form_data.get("order_desc")
|
is_asc = not self.form_data.get("order_desc")
|
||||||
row.sort_values(ascending=is_asc, inplace=True)
|
row.sort_values(ascending=is_asc, inplace=True)
|
||||||
pt = df.pivot_table(index=self.groupby, columns=columns, values=metrics)
|
pt = df.pivot_table(index=groupby, columns=columns, values=metrics)
|
||||||
if self.form_data.get("contribution"):
|
if self.form_data.get("contribution"):
|
||||||
pt = pt.T
|
pt = pt.T
|
||||||
pt = (pt / pt.sum()).T
|
pt = (pt / pt.sum()).T
|
||||||
|
@ -1799,7 +1826,7 @@ class DistributionBarViz(BaseViz):
|
||||||
pt = pt[metrics]
|
pt = pt[metrics]
|
||||||
chart_data = []
|
chart_data = []
|
||||||
for name, ys in pt.items():
|
for name, ys in pt.items():
|
||||||
if pt[name].dtype.kind not in "biufc" or name in self.groupby:
|
if pt[name].dtype.kind not in "biufc" or name in groupby:
|
||||||
continue
|
continue
|
||||||
if isinstance(name, str):
|
if isinstance(name, str):
|
||||||
series_title = name
|
series_title = name
|
||||||
|
@ -1834,7 +1861,7 @@ class SunburstViz(BaseViz):
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
form_data = copy.deepcopy(self.form_data)
|
form_data = copy.deepcopy(self.form_data)
|
||||||
cols = form_data.get("groupby") or []
|
cols = get_column_names(form_data.get("groupby"))
|
||||||
cols.extend(["m1", "m2"])
|
cols.extend(["m1", "m2"])
|
||||||
metric = utils.get_metric_name(form_data["metric"])
|
metric = utils.get_metric_name(form_data["metric"])
|
||||||
secondary_metric = (
|
secondary_metric = (
|
||||||
|
@ -1888,7 +1915,7 @@ class SankeyViz(BaseViz):
|
||||||
def get_data(self, df: pd.DataFrame) -> VizData:
|
def get_data(self, df: pd.DataFrame) -> VizData:
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
source, target = self.groupby
|
source, target = get_column_names(self.groupby)
|
||||||
(value,) = self.metric_labels
|
(value,) = self.metric_labels
|
||||||
df.rename(
|
df.rename(
|
||||||
columns={source: "source", target: "target", value: "value",}, inplace=True,
|
columns={source: "source", target: "target", value: "value",}, inplace=True,
|
||||||
|
@ -1995,7 +2022,7 @@ class CountryMapViz(BaseViz):
|
||||||
def get_data(self, df: pd.DataFrame) -> VizData:
|
def get_data(self, df: pd.DataFrame) -> VizData:
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
cols = [self.form_data.get("entity")]
|
cols = get_column_names([self.form_data.get("entity")]) # type: ignore
|
||||||
metric = self.metric_labels[0]
|
metric = self.metric_labels[0]
|
||||||
cols += [metric]
|
cols += [metric]
|
||||||
ndf = df[cols]
|
ndf = df[cols]
|
||||||
|
@ -2027,7 +2054,7 @@ class WorldMapViz(BaseViz):
|
||||||
# pylint: disable=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from superset.examples import countries
|
from superset.examples import countries
|
||||||
|
|
||||||
cols = [self.form_data.get("entity")]
|
cols = get_column_names([self.form_data.get("entity")]) # type: ignore
|
||||||
metric = utils.get_metric_name(self.form_data["metric"])
|
metric = utils.get_metric_name(self.form_data["metric"])
|
||||||
secondary_metric = (
|
secondary_metric = (
|
||||||
utils.get_metric_name(self.form_data["secondary_metric"])
|
utils.get_metric_name(self.form_data["secondary_metric"])
|
||||||
|
@ -2197,8 +2224,8 @@ class HeatmapViz(BaseViz):
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
x = self.form_data.get("all_columns_x")
|
x = get_column_name(self.form_data.get("all_columns_x")) # type: ignore
|
||||||
y = self.form_data.get("all_columns_y")
|
y = get_column_name(self.form_data.get("all_columns_y")) # type: ignore
|
||||||
v = self.metric_labels[0]
|
v = self.metric_labels[0]
|
||||||
if x == y:
|
if x == y:
|
||||||
df.columns = ["x", "y", "v"]
|
df.columns = ["x", "y", "v"]
|
||||||
|
@ -2814,7 +2841,7 @@ class DeckGeoJson(BaseDeckGLViz):
|
||||||
return query_obj
|
return query_obj
|
||||||
|
|
||||||
def get_properties(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
def get_properties(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
geojson = data[self.form_data["geojson"]]
|
geojson = data[get_column_name(self.form_data["geojson"])]
|
||||||
return json.loads(geojson)
|
return json.loads(geojson)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2922,7 +2949,7 @@ class PairedTTestViz(BaseViz):
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
groups = self.form_data.get("groupby")
|
groups = get_column_names(self.form_data.get("groupby"))
|
||||||
metrics = self.metric_labels
|
metrics = self.metric_labels
|
||||||
df = df.pivot_table(index=DTTM_ALIAS, columns=groups, values=metrics)
|
df = df.pivot_table(index=DTTM_ALIAS, columns=groups, values=metrics)
|
||||||
cols = []
|
cols = []
|
||||||
|
@ -3151,7 +3178,7 @@ class PartitionViz(NVD3TimeSeriesViz):
|
||||||
def get_data(self, df: pd.DataFrame) -> VizData:
|
def get_data(self, df: pd.DataFrame) -> VizData:
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return None
|
return None
|
||||||
groups = self.form_data.get("groupby", [])
|
groups = get_column_names(self.form_data.get("groupby"))
|
||||||
time_op = self.form_data.get("time_series_option", "not_time")
|
time_op = self.form_data.get("time_series_option", "not_time")
|
||||||
if not groups:
|
if not groups:
|
||||||
raise ValueError("Please choose at least one groupby")
|
raise ValueError("Please choose at least one groupby")
|
||||||
|
|
|
@ -38,7 +38,6 @@ from superset.utils.core import get_example_default_schema
|
||||||
|
|
||||||
from tests.integration_tests.base_api_tests import ApiOwnersTestCaseMixin
|
from tests.integration_tests.base_api_tests import ApiOwnersTestCaseMixin
|
||||||
from tests.integration_tests.base_tests import SupersetTestCase
|
from tests.integration_tests.base_tests import SupersetTestCase
|
||||||
from tests.integration_tests.insert_chart_mixin import InsertChartMixin
|
|
||||||
from tests.integration_tests.fixtures.birth_names_dashboard import (
|
from tests.integration_tests.fixtures.birth_names_dashboard import (
|
||||||
load_birth_names_dashboard_with_slices,
|
load_birth_names_dashboard_with_slices,
|
||||||
)
|
)
|
||||||
|
@ -58,6 +57,7 @@ from tests.integration_tests.fixtures.unicode_dashboard import (
|
||||||
from tests.integration_tests.fixtures.world_bank_dashboard import (
|
from tests.integration_tests.fixtures.world_bank_dashboard import (
|
||||||
load_world_bank_dashboard_with_slices,
|
load_world_bank_dashboard_with_slices,
|
||||||
)
|
)
|
||||||
|
from tests.integration_tests.insert_chart_mixin import InsertChartMixin
|
||||||
from tests.integration_tests.test_app import app
|
from tests.integration_tests.test_app import app
|
||||||
from tests.integration_tests.utils.get_dashboards import get_dashboards_ids
|
from tests.integration_tests.utils.get_dashboards import get_dashboards_ids
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ from superset.errors import SupersetErrorType
|
||||||
from superset.extensions import async_query_manager, db
|
from superset.extensions import async_query_manager, db
|
||||||
from superset.models.annotations import AnnotationLayer
|
from superset.models.annotations import AnnotationLayer
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
|
from superset.typing import AdhocColumn
|
||||||
from superset.utils.core import (
|
from superset.utils.core import (
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
get_example_database,
|
get_example_database,
|
||||||
|
@ -61,6 +62,12 @@ from tests.integration_tests.fixtures.query_context import (
|
||||||
|
|
||||||
CHART_DATA_URI = "api/v1/chart/data"
|
CHART_DATA_URI = "api/v1/chart/data"
|
||||||
CHARTS_FIXTURE_COUNT = 10
|
CHARTS_FIXTURE_COUNT = 10
|
||||||
|
ADHOC_COLUMN_FIXTURE: AdhocColumn = {
|
||||||
|
"hasCustomLabel": True,
|
||||||
|
"label": "male_or_female",
|
||||||
|
"sqlExpression": "case when gender = 'boy' then 'male' "
|
||||||
|
"when gender = 'girl' then 'female' else 'other' end",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseTestChartDataApi(SupersetTestCase):
|
class BaseTestChartDataApi(SupersetTestCase):
|
||||||
|
@ -820,3 +827,23 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(rv.status_code, 404)
|
self.assertEqual(rv.status_code, 404)
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||||
|
def test_chart_data_with_adhoc_column(self):
|
||||||
|
"""
|
||||||
|
Chart data API: Test query with adhoc column in both select and where clause
|
||||||
|
"""
|
||||||
|
self.login(username="admin")
|
||||||
|
request_payload = get_query_context("birth_names")
|
||||||
|
request_payload["queries"][0]["columns"] = [ADHOC_COLUMN_FIXTURE]
|
||||||
|
request_payload["queries"][0]["filters"] = [
|
||||||
|
{"col": ADHOC_COLUMN_FIXTURE, "op": "IN", "val": ["male", "female"]}
|
||||||
|
]
|
||||||
|
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
|
||||||
|
response_payload = json.loads(rv.data.decode("utf-8"))
|
||||||
|
result = response_payload["result"][0]
|
||||||
|
data = result["data"]
|
||||||
|
assert {column for column in data[0].keys()} == {"male_or_female", "sum__num"}
|
||||||
|
unique_genders = {row["male_or_female"] for row in data}
|
||||||
|
assert unique_genders == {"male", "female"}
|
||||||
|
assert result["applied_filters"] == [{"column": "male_or_female"}]
|
||||||
|
|
Loading…
Reference in New Issue