feat: standardized form_data (#20010)

This commit is contained in:
Yongjie Zhao
2022-06-05 21:05:01 +08:00
committed by GitHub
parent b08e21efd9
commit dd4b581fb5
11 changed files with 554 additions and 15 deletions

View File

@@ -21,3 +21,4 @@ export * from './getControlConfig';
export * from './getControlState';
export * from './getFormDataFromControls';
export * from './getControlValuesCompatibleWithDatasource';
export * from './standardizedFormData';

View File

@@ -0,0 +1,307 @@
/**
* 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 { 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,
publicControls,
} from './standardizedFormData';
import { xAxisControl } from '../../../plugins/plugin-chart-echarts/src/controls';
describe('should collect control values and create SFD', () => {
const sharedControlsFormData = {};
Object.entries(sharedControls).forEach(([, names]) => {
names.forEach(name => {
sharedControlsFormData[name] = name;
});
});
const publicControlsFormData = Object.fromEntries(
publicControls.map((name, idx) => [[name], idx]),
);
const sourceMockFormData: QueryFormData = {
...sharedControlsFormData,
...publicControlsFormData,
datasource: '100__table',
viz_type: 'source_viz',
};
const sourceMockStore = {
form_data: sourceMockFormData,
controls: Object.fromEntries(
Object.entries(sourceMockFormData).map(([key, value]) => [
key,
{ value },
]),
),
datasource: {
type: 'table',
columns: [],
},
};
beforeAll(() => {
getChartControlPanelRegistry().registerValue('source_viz', {
controlPanelSections: [
sections.advancedAnalyticsControls,
{
label: 'transform controls',
controlSetRows: publicControls.map(control => [control]),
},
{
label: 'axis column',
controlSetRows: [[xAxisControl]],
},
],
});
getChartControlPanelRegistry().registerValue('target_viz', {
controlPanelSections: [
sections.advancedAnalyticsControls,
{
label: 'transform controls',
controlSetRows: publicControls.map(control => [control]),
},
{
label: 'axis column',
controlSetRows: [[xAxisControl]],
},
],
denormalizeFormData: (formData: QueryFormData) => ({
...formData,
columns: formData.standardizedFormData.standardizedState.columns,
metrics: formData.standardizedFormData.standardizedState.metrics,
}),
});
});
test('collect sharedControls', () => {
const sfd = new StandardizedFormData(sourceMockFormData);
expect(sfd.dumpSFD().standardizedState.metrics).toEqual(
sharedControls.metrics.map(controlName => controlName),
);
expect(sfd.dumpSFD().standardizedState.columns).toEqual(
sharedControls.columns.map(controlName => controlName),
);
});
test('should transform all publicControls', () => {
const sfd = new StandardizedFormData(sourceMockFormData);
const { formData } = sfd.transform('target_viz', sourceMockStore);
Object.entries(publicControlsFormData).forEach(([key]) => {
expect(formData).toHaveProperty(key);
});
Object.entries(sharedControls).forEach(([key, value]) => {
expect(formData[key]).toEqual(value);
});
});
test('should inherit standardizedFormData and memorizedFormData is LIFO', () => {
// from source_viz to target_viz
const sfd = new StandardizedFormData(sourceMockFormData);
const { formData, controlsState } = sfd.transform(
'target_viz',
sourceMockStore,
);
expect(
formData.standardizedFormData.memorizedFormData.map(
(fd: [string, QueryFormData]) => fd[0],
),
).toEqual(['source_viz']);
// from target_viz to source_viz
const sfd2 = new StandardizedFormData(formData);
const { formData: fd2, controlsState: cs2 } = sfd2.transform('source_viz', {
...sourceMockStore,
form_data: formData,
controls: controlsState,
});
expect(
fd2.standardizedFormData.memorizedFormData.map(
(fd: [string, QueryFormData]) => fd[0],
),
).toEqual(['source_viz', 'target_viz']);
// from source_viz to target_viz
const sfd3 = new StandardizedFormData(fd2);
const { formData: fd3 } = sfd3.transform('target_viz', {
...sourceMockStore,
form_data: fd2,
controls: cs2,
});
expect(
fd3.standardizedFormData.memorizedFormData.map(
(fd: [string, QueryFormData]) => fd[0],
),
).toEqual(['target_viz', 'source_viz']);
});
});
describe('should transform form_data between table and bigNumberTotal', () => {
const tableVizFormData = {
datasource: '30__table',
viz_type: 'table',
time_grain_sqla: 'P1D',
time_range: 'No filter',
query_mode: 'aggregate',
groupby: ['name'],
metrics: ['count'],
all_columns: [],
percent_metrics: [],
adhoc_filters: [],
order_by_cols: [],
row_limit: 10000,
server_page_length: 10,
order_desc: true,
table_timestamp_format: 'smart_date',
show_cell_bars: true,
color_pn: true,
applied_time_extras: {},
url_params: {
form_data_key:
'p3No_sqDW7k-kMTzlBPAPd9vwp1IXTf6stbyzjlrPPa0ninvdYUUiMC6F1iKit3Y',
dataset_id: '30',
},
};
const tableVizStore = {
form_data: tableVizFormData,
controls: {
datasource: {
value: '30__table',
},
viz_type: {
value: 'table',
},
slice_id: {},
cache_timeout: {},
url_params: {
value: {
form_data_key:
'p3No_sqDW7k-kMTzlBPAPd9vwp1IXTf6stbyzjlrPPa0ninvdYUUiMC6F1iKit3Y',
dataset_id: '30',
},
},
granularity_sqla: {},
time_grain_sqla: {
value: 'P1D',
},
time_range: {
value: 'No filter',
},
query_mode: {
value: 'aggregate',
},
groupby: {
value: ['name'],
},
metrics: {
value: ['count'],
},
all_columns: {
value: [],
},
percent_metrics: {
value: [],
},
adhoc_filters: {
value: [],
},
timeseries_limit_metric: {},
order_by_cols: {
value: [],
},
server_pagination: {},
row_limit: {
value: 10000,
},
server_page_length: {
value: 10,
},
include_time: {},
order_desc: {
value: true,
},
show_totals: {},
emit_filter: {},
table_timestamp_format: {
value: 'smart_date',
},
page_length: {},
include_search: {},
show_cell_bars: {
value: true,
},
align_pn: {},
color_pn: {
value: true,
},
column_config: {},
conditional_formatting: {},
},
datasource: {
type: 'table',
columns: [],
},
};
beforeAll(() => {
getChartControlPanelRegistry().registerValue(
'big_number_total',
new BigNumberTotalChartPlugin().controlPanel,
);
getChartControlPanelRegistry().registerValue(
'table',
new TableChartPlugin().controlPanel,
);
});
test('transform', () => {
// table -> bigNumberTotal
const sfd = new StandardizedFormData(tableVizFormData);
const { formData: bntFormData, controlsState: bntControlsState } =
sfd.transform('big_number_total', tableVizStore);
expect(Object.keys(bntFormData).sort()).toEqual(
[...Object.keys(bntControlsState), 'standardizedFormData'].sort(),
);
expect(bntFormData.viz_type).toBe('big_number_total');
expect(bntFormData.metric).toBe('count');
// change control values
bntFormData.metric = 'sum(sales)';
bntFormData.time_range = '2021 : 2022';
bntControlsState.metric.value = 'sum(sales)';
bntControlsState.time_range.value = '2021 : 2022';
// bigNumberTotal -> table
const sfd2 = new StandardizedFormData(bntFormData);
const { formData: tblFormData, controlsState: tblControlsState } =
sfd2.transform('table', {
...tableVizStore,
form_data: bntFormData,
controls: bntControlsState,
});
expect(Object.keys(tblFormData).sort()).toEqual(
[...Object.keys(tblControlsState), 'standardizedFormData'].sort(),
);
expect(tblFormData.viz_type).toBe('table');
expect(tblFormData.metrics).toEqual(['sum(sales)']);
expect(tblFormData.groupby).toEqual([]);
expect(tblFormData.time_range).toBe('2021 : 2022');
});
});

View File

@@ -0,0 +1,182 @@
/**
* 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 {
ensureIsArray,
getChartControlPanelRegistry,
QueryFormData,
} from '@superset-ui/core';
import {
ControlStateMapping,
StandardizedState,
StandardizedFormDataInterface,
} from '@superset-ui/chart-controls';
import { getControlsState } from 'src/explore/store';
import { getFormDataFromControls } from './getFormDataFromControls';
export const sharedControls: Record<keyof StandardizedState, string[]> = {
metrics: ['metric', 'metrics', 'metric_2'],
columns: ['groupby', 'columns', 'groupbyColumns', 'groupbyRows'],
};
export const publicControls = [
// time section
'granularity_sqla',
'time_grain_sqla',
'time_range',
// filters
'adhoc_filters',
// subquery limit(series limit)
'limit',
// order by clause
'timeseries_limit_metric',
'series_limit_metric',
// desc or asc in order by clause
'order_desc',
// outer query limit
'row_limit',
// x asxs column
'x_axis',
// advanced analytics - rolling window
'rolling_type',
'rolling_periods',
'min_periods',
// advanced analytics - time comparison
'time_compare',
'comparison_type',
// advanced analytics - resample
'resample_rule',
'resample_method',
];
export class StandardizedFormData {
private sfd: StandardizedFormDataInterface;
constructor(sourceFormData: QueryFormData) {
/*
* Support form_data for smooth switching between different viz
* */
const standardizedState = {
metrics: [],
columns: [],
};
const formData = Object.freeze(sourceFormData);
const reversedMap = StandardizedFormData.getReversedMap();
Object.entries(formData).forEach(([key, value]) => {
if (reversedMap.has(key)) {
standardizedState[reversedMap.get(key)].push(...ensureIsArray(value));
}
});
const memorizedFormData = Array.isArray(
formData?.standardizedFormData?.memorizedFormData,
)
? new Map(formData.standardizedFormData.memorizedFormData)
: new Map();
const vizType = formData.viz_type;
if (memorizedFormData.has(vizType)) {
memorizedFormData.delete(vizType);
}
memorizedFormData.set(vizType, formData);
this.sfd = {
standardizedState,
memorizedFormData,
};
}
static getReversedMap() {
const reversedMap = new Map();
Object.entries(sharedControls).forEach(([key, names]) => {
names.forEach(name => {
reversedMap.set(name, key);
});
});
return reversedMap;
}
private getLatestFormData(vizType: string): QueryFormData {
if (this.sfd.memorizedFormData.has(vizType)) {
return this.sfd.memorizedFormData.get(vizType) as QueryFormData;
}
return this.memorizedFormData.slice(-1)[0][1];
}
private get standardizedState() {
return this.sfd.standardizedState;
}
private get memorizedFormData() {
return Array.from(this.sfd.memorizedFormData.entries());
}
dumpSFD() {
return {
standardizedState: this.standardizedState,
memorizedFormData: this.memorizedFormData,
};
}
transform(
targetVizType: string,
exploreState: Record<string, any>,
): {
formData: QueryFormData;
controlsState: ControlStateMapping;
} {
/*
* Transfrom form_data between different viz. Return new form_data and controlsState.
* 1. get memorized form_data by viz type or get previous form_data
* 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
* 6. use final form_data to generate controlsState
* */
const latestFormData = this.getLatestFormData(targetVizType);
const publicFormData = {};
publicControls.forEach(key => {
if (key in exploreState.form_data) {
publicFormData[key] = exploreState.form_data[key];
}
});
const targetControlsState = getControlsState(exploreState, {
...latestFormData,
...publicFormData,
viz_type: targetVizType,
});
const targetFormData = {
...getFormDataFromControls(targetControlsState),
standardizedFormData: this.dumpSFD(),
};
const controlPanel = getChartControlPanelRegistry().get(targetVizType);
if (controlPanel?.denormalizeFormData) {
const transformed = controlPanel.denormalizeFormData(targetFormData);
return {
formData: transformed,
controlsState: getControlsState(exploreState, transformed),
};
}
return {
formData: targetFormData,
controlsState: targetControlsState,
};
}
}