refactor(standardized form data): refine interface and improve code smells (#20518)

This commit is contained in:
Yongjie Zhao 2022-06-28 21:09:42 +08:00 committed by GitHub
parent 23e62d3782
commit c348a095b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 580 additions and 224 deletions

View File

@ -344,30 +344,36 @@ export interface ControlPanelSectionConfig {
controlSetRows: ControlSetRow[];
}
export interface StandardizedState {
export interface StandardizedControls {
metrics: QueryFormMetric[];
columns: QueryFormColumn[];
}
export interface StandardizedFormDataInterface {
standardizedState: StandardizedState;
// Controls not used in the current viz
controls: StandardizedControls;
// Transformation history
memorizedFormData: Map<string, QueryFormData>;
}
export type QueryStandardizedFormData = QueryFormData & {
standardizedFormData: StandardizedFormDataInterface;
};
export const isStandardizedFormData = (
formData: QueryFormData,
): formData is QueryStandardizedFormData =>
formData?.standardizedFormData?.controls &&
formData?.standardizedFormData?.memorizedFormData &&
Array.isArray(formData.standardizedFormData.controls.metrics) &&
Array.isArray(formData.standardizedFormData.controls.columns);
export interface ControlPanelConfig {
controlPanelSections: (ControlPanelSectionConfig | null)[];
controlOverrides?: ControlOverrides;
sectionOverrides?: SectionOverrides;
onInit?: (state: ControlStateMapping) => void;
denormalizeFormData?: (
formData: QueryFormData & {
standardizedFormData: StandardizedFormDataInterface;
},
) => QueryFormData;
updateStandardizedState?: (
prevState: StandardizedState,
currState: StandardizedState,
) => StandardizedState;
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
}
export type ControlOverrides = {

View File

@ -0,0 +1,64 @@
/**
* 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 { makeSingleton, QueryFormData } from '@superset-ui/core';
import { isStandardizedFormData, StandardizedControls } from '../types';
class StandardizedControlsManager {
controls: StandardizedControls;
constructor() {
this.controls = {
metrics: [],
columns: [],
};
}
setStandardizedControls(formData: QueryFormData) {
if (isStandardizedFormData(formData)) {
const { controls } = formData.standardizedFormData;
this.controls = {
metrics: controls.metrics,
columns: controls.columns,
};
}
}
shiftMetric() {
return this.controls.metrics.shift();
}
popAllMetrics() {
return this.controls.metrics.splice(0, this.controls.metrics.length);
}
popAllColumns() {
return this.controls.columns.splice(0, this.controls.columns.length);
}
clear() {
this.controls = {
metrics: [],
columns: [],
};
}
}
export const getStandardizedControls = makeSingleton(
StandardizedControlsManager,
);

View File

@ -23,3 +23,4 @@ export * from './getColorFormatters';
export { default as mainMetric } from './mainMetric';
export { default as columnChoices } from './columnChoices';
export * from './defineSavedMetrics';
export * from './getStandardizedControls';

View File

@ -0,0 +1,80 @@
/**
* 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 { QueryFormData } from '@superset-ui/core';
import { getStandardizedControls } from '../../src';
const formData: QueryFormData = {
datasource: '30__table',
viz_type: 'table',
standardizedFormData: {
controls: {
metrics: ['count(*)', 'sum(sales)'],
columns: ['gender', 'gender'],
},
memorizedFormData: [],
},
};
test('without standardizedFormData', () => {
getStandardizedControls().setStandardizedControls({
datasource: '30__table',
viz_type: 'table',
});
expect(getStandardizedControls().controls).toEqual({
metrics: [],
columns: [],
});
});
test('getStandardizedControls', () => {
expect(getStandardizedControls().controls).toEqual({
metrics: [],
columns: [],
});
getStandardizedControls().setStandardizedControls(formData);
expect(getStandardizedControls().controls).toEqual({
metrics: ['count(*)', 'sum(sales)'],
columns: ['gender', 'gender'],
});
expect(getStandardizedControls().shiftMetric()).toEqual('count(*)');
expect(getStandardizedControls().controls).toEqual({
metrics: ['sum(sales)'],
columns: ['gender', 'gender'],
});
expect(getStandardizedControls().popAllMetrics()).toEqual(['sum(sales)']);
expect(getStandardizedControls().controls).toEqual({
metrics: [],
columns: ['gender', 'gender'],
});
expect(getStandardizedControls().popAllColumns()).toEqual([
'gender',
'gender',
]);
expect(getStandardizedControls().controls).toEqual({
metrics: [],
columns: [],
});
getStandardizedControls().setStandardizedControls(formData);
getStandardizedControls().clear();
expect(getStandardizedControls().controls).toEqual({
metrics: [],
columns: [],
});
});

View File

@ -19,6 +19,7 @@
*/
import { GenericDataType } from './QueryResponse';
import { QueryFormColumn } from './QueryFormData';
export interface AdhocColumn {
hasCustomLabel?: boolean;
@ -53,12 +54,18 @@ export interface Column {
export default {};
export function isPhysicalColumn(
column?: AdhocColumn | PhysicalColumn,
): column is PhysicalColumn {
export function isPhysicalColumn(column?: any): column is PhysicalColumn {
return typeof column === 'string';
}
export function isAdhocColumn(column?: AdhocColumn | PhysicalColumn) {
return (column as AdhocColumn)?.sqlExpression !== undefined;
export function isAdhocColumn(column?: any): column is AdhocColumn {
return (
typeof column !== 'string' &&
column?.sqlExpression !== undefined &&
column?.expressionType === 'SQL'
);
}
export function isQueryFormColumn(column: any): column is QueryFormColumn {
return isPhysicalColumn(column) || isAdhocColumn(column);
}

View File

@ -17,7 +17,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Maybe } from '../../types';
import { Maybe, QueryFormMetric } from '../../types';
import { Column } from './Column';
export type Aggregate =
@ -74,8 +74,22 @@ export interface Metric {
export default {};
export function isAdhocMetricSimple(
metric: AdhocMetric,
): metric is AdhocMetricSimple {
return metric.expressionType === 'SIMPLE';
export function isSavedMetric(metric: any): metric is SavedMetric {
return typeof metric === 'string';
}
export function isAdhocMetricSimple(metric: any): metric is AdhocMetricSimple {
return typeof metric !== 'string' && metric?.expressionType === 'SIMPLE';
}
export function isAdhocMetricSQL(metric: any): metric is AdhocMetricSQL {
return typeof metric !== 'string' && metric?.expressionType === 'SQL';
}
export function isQueryFormMetric(metric: any): metric is QueryFormMetric {
return (
isSavedMetric(metric) ||
isAdhocMetricSimple(metric) ||
isAdhocMetricSQL(metric)
);
}

View File

@ -220,8 +220,4 @@ export function isDruidFormData(
return 'granularity' in formData;
}
export function isSavedMetric(metric: QueryFormMetric): metric is SavedMetric {
return typeof metric === 'string';
}
export default {};

View File

@ -0,0 +1,63 @@
/**
* 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 {
isAdhocColumn,
isPhysicalColumn,
isQueryFormColumn,
} from '@superset-ui/core';
const adhocColumn = {
expressionType: 'SQL',
label: 'country',
optionName: 'country',
sqlExpression: 'country',
};
test('isPhysicalColumn returns true', () => {
expect(isPhysicalColumn('gender')).toEqual(true);
});
test('isPhysicalColumn returns false', () => {
expect(isPhysicalColumn(adhocColumn)).toEqual(false);
});
test('isAdhocColumn returns true', () => {
expect(isAdhocColumn(adhocColumn)).toEqual(true);
});
test('isAdhocColumn returns false', () => {
expect(isAdhocColumn('hello')).toEqual(false);
expect(isAdhocColumn({})).toEqual(false);
expect(
isAdhocColumn({
expressionType: 'SQL',
label: 'country',
optionName: 'country',
}),
).toEqual(false);
});
test('isQueryFormColumn returns true', () => {
expect(isQueryFormColumn('gender')).toEqual(true);
expect(isQueryFormColumn(adhocColumn)).toEqual(true);
});
test('isQueryFormColumn returns false', () => {
expect(isQueryFormColumn({})).toEqual(false);
});

View File

@ -0,0 +1,86 @@
/**
* 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 {
isSavedMetric,
isAdhocMetricSimple,
isAdhocMetricSQL,
isQueryFormMetric,
} from '@superset-ui/core';
const adhocMetricSimple = {
expressionType: 'SIMPLE',
column: {
id: 1,
column_name: 'sales',
columnName: 'sales',
verbose_name: 'sales',
},
aggregate: 'SUM',
label: 'count',
optionName: 'count',
};
const adhocMetricSQL = {
expressionType: 'SQL',
label: 'count',
optionName: 'count',
sqlExpression: 'count(*)',
};
test('isSavedMetric returns true', () => {
expect(isSavedMetric('count(*)')).toEqual(true);
});
test('isSavedMetric returns false', () => {
expect(isSavedMetric(adhocMetricSQL)).toEqual(false);
expect(isSavedMetric(null)).toEqual(false);
expect(isSavedMetric(undefined)).toEqual(false);
});
test('isAdhocMetricSimple returns true', () => {
expect(isAdhocMetricSimple(adhocMetricSimple)).toEqual(true);
});
test('isAdhocMetricSimple returns false', () => {
expect(isAdhocMetricSimple('hello')).toEqual(false);
expect(isAdhocMetricSimple({})).toEqual(false);
expect(isAdhocMetricSimple(adhocMetricSQL)).toEqual(false);
});
test('isAdhocMetricSQL returns true', () => {
expect(isAdhocMetricSQL(adhocMetricSQL)).toEqual(true);
});
test('isAdhocMetricSQL returns false', () => {
expect(isAdhocMetricSQL('hello')).toEqual(false);
expect(isAdhocMetricSQL({})).toEqual(false);
expect(isAdhocMetricSQL(adhocMetricSimple)).toEqual(false);
});
test('isQueryFormMetric returns true', () => {
expect(isQueryFormMetric(adhocMetricSQL)).toEqual(true);
expect(isQueryFormMetric(adhocMetricSimple)).toEqual(true);
expect(isQueryFormMetric('count(*)')).toEqual(true);
});
test('isQueryFormMetric returns false', () => {
expect(isQueryFormMetric({})).toEqual(false);
expect(isQueryFormMetric(undefined)).toEqual(false);
expect(isQueryFormMetric(null)).toEqual(false);
});

View File

@ -20,6 +20,7 @@ import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
formatSelectOptions,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
import { ColorBy } from './utils';
@ -152,9 +153,9 @@ const config: ControlPanelConfig = {
Boolean(controls?.color_by.value === ColorBy.country),
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
metrics: getStandardizedControls().popAllMetrics(),
}),
};

View File

@ -19,6 +19,7 @@
import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -122,10 +123,10 @@ const config: ControlPanelConfig = {
timeSeriesSection[1],
sections.annotations,
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
sections,
sharedControls,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import {
showLegend,
@ -133,14 +134,18 @@ const config: ControlPanelConfig = {
rerender: ['groupby'],
},
},
denormalizeFormData: formData => {
const columns =
formData.standardizedFormData.standardizedState.columns.filter(
col => !ensureIsArray(formData.groupby).includes(col),
formDataOverrides: formData => {
const columns = getStandardizedControls().controls.columns.filter(
col => !ensureIsArray(formData.groupby).includes(col),
);
getStandardizedControls().controls.columns =
getStandardizedControls().controls.columns.filter(
col => !columns.includes(col),
);
return {
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
metrics: getStandardizedControls().popAllMetrics(),
columns,
};
},

View File

@ -17,7 +17,11 @@
* under the License.
*/
import { t } from '@superset-ui/core';
import { ControlPanelConfig, sections } from '@superset-ui/chart-controls';
import {
ControlPanelConfig,
sections,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import {
lineInterpolation,
showBrush,
@ -96,10 +100,10 @@ const config: ControlPanelConfig = {
default: 50000,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -21,6 +21,7 @@ import {
ControlPanelConfig,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
@ -96,12 +97,8 @@ export default {
label: t('Number format'),
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
}),
} as ControlPanelConfig;

View File

@ -22,6 +22,7 @@ import {
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
formatSelectOptions,
getStandardizedControls,
sections,
} from '@superset-ui/chart-controls';
import React from 'react';
@ -270,13 +271,9 @@ const config: ControlPanelConfig = {
label: t('Number format'),
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
}),
};

View File

@ -25,6 +25,7 @@ import {
sections,
emitFilterControl,
ControlPanelConfig,
getStandardizedControls,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
@ -136,14 +137,18 @@ const config: ControlPanelConfig = {
),
},
},
denormalizeFormData: formData => {
const groupby =
formData.standardizedFormData.standardizedState.columns.filter(
col => !ensureIsArray(formData.columns).includes(col),
formDataOverrides: formData => {
const groupby = getStandardizedControls().controls.columns.filter(
col => !ensureIsArray(formData.columns).includes(col),
);
getStandardizedControls().controls.columns =
getStandardizedControls().controls.columns.filter(
col => !groupby.includes(col),
);
return {
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
metrics: getStandardizedControls().popAllMetrics(),
groupby,
};
},

View File

@ -25,6 +25,7 @@ import {
sharedControls,
ControlStateMapping,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA, EchartsFunnelLabelTypeType } from './types';
import { legendSection } from '../controls';
@ -143,14 +144,10 @@ const config: ControlPanelConfig = {
},
};
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
groupby: formData.standardizedFormData.standardizedState.columns,
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -24,6 +24,7 @@ import {
D3_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@ -308,14 +309,10 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
groupby: formData.standardizedFormData.standardizedState.columns,
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -20,6 +20,7 @@ import React from 'react';
import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -320,13 +321,9 @@ const controlPanel: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().popAllMetrics(),
}),
};

View File

@ -30,6 +30,7 @@ import {
ControlSetRow,
CustomControlItem,
emitFilterControl,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -449,15 +450,23 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => {
const groupby =
formData.standardizedFormData.standardizedState.columns.filter(
col => !ensureIsArray(formData.groupby_b).includes(col),
formDataOverrides: formData => {
const groupby = getStandardizedControls().controls.columns.filter(
col => !ensureIsArray(formData.groupby_b).includes(col),
);
getStandardizedControls().controls.columns =
getStandardizedControls().controls.columns.filter(
col => !groupby.includes(col),
);
const metrics =
formData.standardizedFormData.standardizedState.metrics.filter(
metric => !ensureIsArray(formData.metrics_b).includes(metric),
const metrics = getStandardizedControls().controls.metrics.filter(
metric => !ensureIsArray(formData.metrics_b).includes(metric),
);
getStandardizedControls().controls.metrics =
getStandardizedControls().controls.metrics.filter(
col => !metrics.includes(col),
);
return {
...formData,
metrics,

View File

@ -26,6 +26,7 @@ import {
D3_TIME_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
import { legendSection } from '../controls';
@ -253,17 +254,13 @@ const config: ControlPanelConfig = {
default: 100,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
groupby: formData.standardizedFormData.standardizedState.columns,
metric: getStandardizedControls().shiftMetric(),
groupby: getStandardizedControls().popAllColumns(),
row_limit:
ensureIsInt(formData.row_limit, 100) >= 100 ? 100 : formData.row_limit,
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
}),
};
export default config;

View File

@ -33,6 +33,7 @@ import {
sharedControls,
emitFilterControl,
ControlFormItemSpec,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
import { LABEL_POSITION } from '../constants';
@ -210,10 +211,10 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -276,10 +277,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -25,6 +25,7 @@ import {
ControlStateMapping,
D3_TIME_FORMAT_DOCS,
formatSelectOptions,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -328,10 +329,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -263,10 +264,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -207,10 +208,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -207,10 +208,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -22,6 +22,7 @@ import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -260,10 +261,10 @@ const config: ControlPanelConfig = {
default: rowLimit,
},
},
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -20,6 +20,7 @@ import React from 'react';
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import {
ControlPanelConfig,
getStandardizedControls,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@ -285,13 +286,9 @@ const controlPanel: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
}),
};

View File

@ -25,6 +25,7 @@ import {
D3_TIME_FORMAT_OPTIONS,
sections,
emitFilterControl,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@ -137,14 +138,10 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metric: formData.standardizedFormData.standardizedState.metrics[0],
groupby: formData.standardizedFormData.standardizedState.columns,
}),
updateStandardizedState: (prevState, currState) => ({
...currState,
metrics: [currState.metrics[0], ...prevState.metrics.slice(1)],
metric: getStandardizedControls().shiftMetric(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -32,6 +32,7 @@ import {
sharedControls,
emitFilterControl,
Dataset,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { MetricsLayoutEnum } from '../types';
@ -373,14 +374,17 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => {
const groupbyColumns =
formData.standardizedFormData.standardizedState.columns.filter(
col => !ensureIsArray(formData.groupbyRows).includes(col),
formDataOverrides: formData => {
const groupbyColumns = getStandardizedControls().controls.columns.filter(
col => !ensureIsArray(formData.groupbyRows).includes(col),
);
getStandardizedControls().controls.columns =
getStandardizedControls().controls.columns.filter(
col => !groupbyColumns.includes(col),
);
return {
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
metrics: getStandardizedControls().popAllMetrics(),
groupbyColumns,
};
},

View File

@ -47,6 +47,7 @@ import {
Dataset,
ColumnMeta,
defineSavedMetrics,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import i18n from './i18n';
@ -544,10 +545,10 @@ const config: ControlPanelConfig = {
],
},
],
denormalizeFormData: formData => ({
formDataOverrides: formData => ({
...formData,
metrics: formData.standardizedFormData.standardizedState.metrics,
groupby: formData.standardizedFormData.standardizedState.columns,
metrics: getStandardizedControls().popAllMetrics(),
groupby: getStandardizedControls().popAllColumns(),
}),
};

View File

@ -16,27 +16,65 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getChartControlPanelRegistry, QueryFormData } from '@superset-ui/core';
import {
AdhocColumn,
AdhocMetricSimple,
AdhocMetricSQL,
getChartControlPanelRegistry,
QueryFormData,
} from '@superset-ui/core';
import TableChartPlugin from '@superset-ui/plugin-chart-table';
import { BigNumberTotalChartPlugin } from '@superset-ui/plugin-chart-echarts';
import { sections } from '@superset-ui/chart-controls';
import {
StandardizedFormData,
sharedControls,
sharedMetricsKey,
sharedColumnsKey,
publicControls,
} from './standardizedFormData';
const adhocColumn: AdhocColumn = {
expressionType: 'SQL',
label: 'country',
optionName: 'country',
sqlExpression: 'country',
};
const adhocMetricSQL: AdhocMetricSQL = {
expressionType: 'SQL',
label: 'count',
optionName: 'count',
sqlExpression: 'count(*)',
};
const adhocMetricSimple: AdhocMetricSimple = {
expressionType: 'SIMPLE',
column: {
id: 1,
column_name: 'sales',
columnName: 'sales',
verbose_name: 'sales',
},
aggregate: 'SUM',
label: 'count',
optionName: 'count',
};
describe('should collect control values and create SFD', () => {
const sharedKey = [...sharedMetricsKey, ...sharedColumnsKey];
const sharedControlsFormData = {
// metrics
metric: 'm1',
metrics: ['m2'],
metric_2: 'm3',
size: 'm4',
// columns
groupby: ['c1'],
columns: ['c2'],
groupbyColumns: ['c3'],
groupbyRows: ['c4'],
series: 'c5',
entity: 'c6',
};
const publicControlsFormData = {
// time section
@ -112,16 +150,16 @@ describe('should collect control values and create SFD', () => {
controlSetRows: [['x_axis']],
},
],
denormalizeFormData: (formData: QueryFormData) => ({
formDataOverrides: (formData: QueryFormData) => ({
...formData,
columns: formData.standardizedFormData.standardizedState.columns,
metrics: formData.standardizedFormData.standardizedState.metrics,
columns: formData.standardizedFormData.controls.columns,
metrics: formData.standardizedFormData.controls.metrics,
}),
});
});
test('should avoid to overlap', () => {
const sharedControlsSet = new Set(Object.keys(sharedControls));
const sharedControlsSet = new Set(Object.keys(sharedKey));
const publicControlsSet = new Set(publicControls);
expect(
[...sharedControlsSet].filter((x: string) => publicControlsSet.has(x)),
@ -130,19 +168,17 @@ describe('should collect control values and create SFD', () => {
test('should collect all sharedControls', () => {
expect(Object.entries(sharedControlsFormData).length).toBe(
Object.entries(sharedControls).length,
Object.entries(sharedKey).length,
);
const sfd = new StandardizedFormData(sourceMockFormData);
expect(sfd.serialize().standardizedState.metrics).toEqual([
'm1',
'm2',
'm3',
]);
expect(sfd.serialize().standardizedState.columns).toEqual([
expect(sfd.serialize().controls.metrics).toEqual(['m1', 'm2', 'm3', 'm4']);
expect(sfd.serialize().controls.columns).toEqual([
'c1',
'c2',
'c3',
'c4',
'c5',
'c6',
]);
});
@ -157,8 +193,8 @@ describe('should collect control values and create SFD', () => {
expect(formData).toHaveProperty(key);
expect(value).toEqual(publicControlsFormData[key]);
});
expect(formData.columns).toEqual(['c1', 'c2', 'c3', 'c4']);
expect(formData.metrics).toEqual(['m1', 'm2', 'm3']);
expect(formData.columns).toEqual(['c1', 'c2', 'c3', 'c4', 'c5', 'c6']);
expect(formData.metrics).toEqual(['m1', 'm2', 'm3', 'm4']);
});
test('should inherit standardizedFormData and memorizedFormData is LIFO', () => {
@ -210,8 +246,8 @@ describe('should transform form_data between table and bigNumberTotal', () => {
time_grain_sqla: 'P1D',
time_range: 'No filter',
query_mode: 'aggregate',
groupby: ['name'],
metrics: ['count'],
groupby: ['name', 'gender', adhocColumn],
metrics: ['count', 'avg(sales)', adhocMetricSimple, adhocMetricSQL],
all_columns: [],
percent_metrics: [],
adhoc_filters: [],
@ -259,10 +295,10 @@ describe('should transform form_data between table and bigNumberTotal', () => {
value: 'aggregate',
},
groupby: {
value: ['name'],
value: ['name', 'gender', adhocColumn],
},
metrics: {
value: ['count'],
value: ['count', 'avg(sales)', adhocMetricSimple, adhocMetricSQL],
},
all_columns: {
value: [],
@ -349,7 +385,7 @@ describe('should transform form_data between table and bigNumberTotal', () => {
expect(bntFormData.viz_type).toBe('big_number_total');
expect(bntFormData.metric).toBe('count');
// change control values
// change control values on bigNumber
bntFormData.metric = 'sum(sales)';
bntFormData.time_range = '2021 : 2022';
bntControlsState.metric.value = 'sum(sales)';
@ -367,8 +403,13 @@ describe('should transform form_data between table and bigNumberTotal', () => {
[...Object.keys(tblControlsState), 'standardizedFormData'].sort(),
);
expect(tblFormData.viz_type).toBe('table');
expect(tblFormData.metrics).toEqual(['sum(sales)']);
expect(tblFormData.groupby).toEqual(['name']);
expect(tblFormData.metrics).toEqual([
'sum(sales)',
'avg(sales)',
adhocMetricSimple,
adhocMetricSQL,
]);
expect(tblFormData.groupby).toEqual(['name', 'gender', adhocColumn]);
expect(tblFormData.time_range).toBe('2021 : 2022');
});
});

View File

@ -16,38 +16,38 @@
* specific language governing permissions and limitations
* under the License.
*/
import { isEmpty, intersection } from 'lodash';
import {
ensureIsArray,
getChartControlPanelRegistry,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
import {
ControlStateMapping,
StandardizedState,
getStandardizedControls,
isStandardizedFormData,
StandardizedControls,
StandardizedFormDataInterface,
} from '@superset-ui/chart-controls';
import { getControlsState } from 'src/explore/store';
import { getFormDataFromControls } from './getFormDataFromControls';
export const sharedControls: Record<string, keyof StandardizedState> = {
// metrics
metric: 'metrics', // via sharedControls, scalar
metrics: 'metrics', // via sharedControls, array
metric_2: 'metrics', // via sharedControls, scalar
// columns
groupby: 'columns', // via sharedControls, array
columns: 'columns', // via sharedControls, array
groupbyColumns: 'columns', // via pivot table v2, array
groupbyRows: 'columns', // via pivot table v2, array
};
const sharedControlsMap: Record<keyof StandardizedState, string[]> = {
metrics: [],
columns: [],
};
Object.entries(sharedControls).forEach(([key, value]) =>
sharedControlsMap[value].push(key),
);
export const sharedMetricsKey = [
'metric', // via sharedControls, scalar
'metrics', // via sharedControls, array
'metric_2', // via sharedControls, scalar
'size', // via sharedControls, scalar
];
export const sharedColumnsKey = [
'groupby', // via sharedControls, array
'columns', // via sharedControls, array
'groupbyColumns', // via pivot table v2, array
'groupbyRows', // via pivot table v2, array
'entity', // via sharedControls, scalar
'series', // via sharedControls, scalar
];
export const publicControls = [
// time section
'granularity_sqla', // via sharedControls
@ -100,65 +100,41 @@ export class StandardizedFormData {
memorizedFormData.set(vizType, formData);
// calculate sharedControls
const standardizedState =
StandardizedFormData.getStandardizedState(formData);
const controls = StandardizedFormData.getStandardizedControls(formData);
this.sfd = {
standardizedState,
controls,
memorizedFormData,
};
}
static getStandardizedState(formData: QueryFormData): StandardizedState {
// 1. collect current sharedControls
let currState: StandardizedState = {
static getStandardizedControls(
formData: QueryFormData,
): StandardizedControls {
// 1. initial StandardizedControls
const controls: StandardizedControls = {
metrics: [],
columns: [],
};
// 2. collect current sharedControls
Object.entries(formData).forEach(([key, value]) => {
if (key in sharedControls) {
currState[sharedControls[key]].push(...ensureIsArray(value));
if (sharedMetricsKey.includes(key)) {
controls.metrics.push(...ensureIsArray<QueryFormMetric>(value));
}
if (sharedColumnsKey.includes(key)) {
controls.columns.push(...ensureIsArray<QueryFormColumn>(value));
}
});
// 2. get previous StandardizedState
let prevState: StandardizedState = {
metrics: [],
columns: [],
};
if (
formData?.standardizedFormData?.standardizedState &&
Array.isArray(formData.standardizedFormData.standardizedState.metrics) &&
Array.isArray(formData.standardizedFormData.standardizedState.columns)
) {
prevState = formData.standardizedFormData.standardizedState;
}
// the initial prevState should equal to currentState
if (isEmpty(prevState.metrics) && isEmpty(prevState.columns)) {
prevState = currState;
// 3. append inherit sharedControls
if (isStandardizedFormData(formData)) {
const { metrics, columns } = formData.standardizedFormData.controls;
controls.metrics.push(...metrics);
controls.columns.push(...columns);
}
// 3. inherit SS from previous state if current viz hasn't columns-like controls or metrics-like controls
Object.keys(sharedControlsMap).forEach(key => {
if (
isEmpty(intersection(Object.keys(formData), sharedControlsMap[key]))
) {
currState[key] = prevState[key];
}
});
// 4. update hook
const controlPanel = getChartControlPanelRegistry().get(formData.viz_type);
if (controlPanel?.updateStandardizedState) {
currState = controlPanel.updateStandardizedState(prevState, currState);
}
// 5. clear up
Object.entries(currState).forEach(([key, value]) => {
currState[key] = value.filter(Boolean);
});
return currState;
return controls;
}
private getLatestFormData(vizType: string): QueryFormData {
@ -169,8 +145,8 @@ export class StandardizedFormData {
return this.memorizedFormData.slice(-1)[0][1];
}
private get standardizedState() {
return this.sfd.standardizedState;
private get standardizedControls() {
return this.sfd.controls;
}
private get memorizedFormData() {
@ -179,7 +155,7 @@ export class StandardizedFormData {
serialize() {
return {
standardizedState: this.standardizedState,
controls: this.standardizedControls,
memorizedFormData: this.memorizedFormData,
};
}
@ -205,7 +181,7 @@ export class StandardizedFormData {
* 2. collect public control values
* 3. generate initial targetControlsState
* 4. attach `standardizedFormData` to the initial form_data
* 5. call denormalizeFormData to transform initial form_data if the plugin was defined
* 5. call formDataOverrides to transform initial form_data if the plugin was defined
* 6. use final form_data to generate controlsState
* */
const latestFormData = this.getLatestFormData(targetVizType);
@ -226,8 +202,17 @@ export class StandardizedFormData {
};
const controlPanel = getChartControlPanelRegistry().get(targetVizType);
if (controlPanel?.denormalizeFormData) {
const transformed = controlPanel.denormalizeFormData(targetFormData);
if (controlPanel?.formDataOverrides) {
getStandardizedControls().setStandardizedControls(targetFormData);
const transformed = {
...controlPanel.formDataOverrides(targetFormData),
standardizedFormData: {
controls: { ...getStandardizedControls().controls },
memorizedFormData: this.memorizedFormData,
},
};
getStandardizedControls().clear();
return {
formData: transformed,
controlsState: getControlsState(exploreState, transformed),