mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
feat(viz-type): Ag grid table plugin Integration (#33517)
Signed-off-by: hainenber <dotronghai96@gmail.com> Co-authored-by: Amaan Nawab <nelsondrew07@gmail.com> Co-authored-by: Levis Mbote <111055098+LevisNgigi@users.noreply.github.com> Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Co-authored-by: Paul Rhodes <withnale@users.noreply.github.com> Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com> Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* 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 {
|
||||
AdhocColumn,
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
isPhysicalColumn,
|
||||
QueryFormOrderBy,
|
||||
QueryMode,
|
||||
QueryObject,
|
||||
removeDuplicates,
|
||||
PostProcessingRule,
|
||||
BuildQuery,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
isTimeComparison,
|
||||
timeCompareOperator,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { TableChartFormData } from './types';
|
||||
import { updateTableOwnState } from './utils/externalAPIs';
|
||||
|
||||
/**
|
||||
* Infer query mode from form data. If `all_columns` is set, then raw records mode,
|
||||
* otherwise defaults to aggregation mode.
|
||||
*
|
||||
* The same logic is used in `controlPanel` with control values as well.
|
||||
*/
|
||||
export function getQueryMode(formData: TableChartFormData) {
|
||||
const { query_mode: mode } = formData;
|
||||
if (mode === QueryMode.Aggregate || mode === QueryMode.Raw) {
|
||||
return mode;
|
||||
}
|
||||
const rawColumns = formData?.all_columns;
|
||||
const hasRawColumns = rawColumns && rawColumns.length > 0;
|
||||
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
|
||||
}
|
||||
|
||||
const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
formData: TableChartFormData,
|
||||
options,
|
||||
) => {
|
||||
const {
|
||||
percent_metrics: percentMetrics,
|
||||
order_desc: orderDesc = false,
|
||||
extra_form_data,
|
||||
} = formData;
|
||||
const queryMode = getQueryMode(formData);
|
||||
const sortByMetric = ensureIsArray(formData.timeseries_limit_metric)[0];
|
||||
const time_grain_sqla =
|
||||
extra_form_data?.time_grain_sqla || formData.time_grain_sqla;
|
||||
let formDataCopy = formData;
|
||||
// never include time in raw records mode
|
||||
if (queryMode === QueryMode.Raw) {
|
||||
formDataCopy = {
|
||||
...formData,
|
||||
include_time: false,
|
||||
};
|
||||
}
|
||||
|
||||
const addComparisonPercentMetrics = (metrics: string[], suffixes: string[]) =>
|
||||
metrics.reduce<string[]>((acc, metric) => {
|
||||
const newMetrics = suffixes.map(suffix => `${metric}__${suffix}`);
|
||||
return acc.concat([metric, ...newMetrics]);
|
||||
}, []);
|
||||
|
||||
return buildQueryContext(formDataCopy, baseQueryObject => {
|
||||
let { metrics, orderby = [], columns = [] } = baseQueryObject;
|
||||
const { extras = {} } = baseQueryObject;
|
||||
let postProcessing: PostProcessingRule[] = [];
|
||||
const nonCustomNorInheritShifts = ensureIsArray(
|
||||
formData.time_compare,
|
||||
).filter((shift: string) => shift !== 'custom' && shift !== 'inherit');
|
||||
const customOrInheritShifts = ensureIsArray(formData.time_compare).filter(
|
||||
(shift: string) => shift === 'custom' || shift === 'inherit',
|
||||
);
|
||||
|
||||
let timeOffsets: string[] = [];
|
||||
|
||||
// Shifts for non-custom or non inherit time comparison
|
||||
if (
|
||||
isTimeComparison(formData, baseQueryObject) &&
|
||||
!isEmpty(nonCustomNorInheritShifts)
|
||||
) {
|
||||
timeOffsets = nonCustomNorInheritShifts;
|
||||
}
|
||||
|
||||
// Shifts for custom or inherit time comparison
|
||||
if (
|
||||
isTimeComparison(formData, baseQueryObject) &&
|
||||
!isEmpty(customOrInheritShifts)
|
||||
) {
|
||||
if (customOrInheritShifts.includes('custom')) {
|
||||
timeOffsets = timeOffsets.concat([formData.start_date_offset]);
|
||||
}
|
||||
if (customOrInheritShifts.includes('inherit')) {
|
||||
timeOffsets = timeOffsets.concat(['inherit']);
|
||||
}
|
||||
}
|
||||
|
||||
let temporalColumnAdded = false;
|
||||
let temporalColumn = null;
|
||||
|
||||
if (queryMode === QueryMode.Aggregate) {
|
||||
metrics = metrics || [];
|
||||
// override orderby with timeseries metric when in aggregation mode
|
||||
if (sortByMetric) {
|
||||
orderby = [[sortByMetric, !orderDesc]];
|
||||
} else if (metrics?.length > 0) {
|
||||
// default to ordering by first metric in descending order
|
||||
// when no "sort by" metric is set (regardless if "SORT DESC" is set to true)
|
||||
orderby = [[metrics[0], false]];
|
||||
}
|
||||
// add postprocessing for percent metrics only when in aggregation mode
|
||||
if (percentMetrics && percentMetrics.length > 0) {
|
||||
const percentMetricsLabelsWithTimeComparison = isTimeComparison(
|
||||
formData,
|
||||
baseQueryObject,
|
||||
)
|
||||
? addComparisonPercentMetrics(
|
||||
percentMetrics.map(getMetricLabel),
|
||||
timeOffsets,
|
||||
)
|
||||
: percentMetrics.map(getMetricLabel);
|
||||
const percentMetricLabels = removeDuplicates(
|
||||
percentMetricsLabelsWithTimeComparison,
|
||||
);
|
||||
metrics = removeDuplicates(
|
||||
metrics.concat(percentMetrics),
|
||||
getMetricLabel,
|
||||
);
|
||||
postProcessing = [
|
||||
{
|
||||
operation: 'contribution',
|
||||
options: {
|
||||
columns: percentMetricLabels,
|
||||
rename_columns: percentMetricLabels.map(x => `%${x}`),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
// Add the operator for the time comparison if some is selected
|
||||
if (!isEmpty(timeOffsets)) {
|
||||
postProcessing.push(timeCompareOperator(formData, baseQueryObject));
|
||||
}
|
||||
|
||||
const temporalColumnsLookup = formData?.temporal_columns_lookup;
|
||||
// Filter out the column if needed and prepare the temporal column object
|
||||
|
||||
columns = columns.filter(col => {
|
||||
const shouldBeAdded =
|
||||
isPhysicalColumn(col) &&
|
||||
time_grain_sqla &&
|
||||
temporalColumnsLookup?.[col];
|
||||
|
||||
if (shouldBeAdded && !temporalColumnAdded) {
|
||||
temporalColumn = {
|
||||
timeGrain: time_grain_sqla,
|
||||
columnType: 'BASE_AXIS',
|
||||
sqlExpression: col,
|
||||
label: col,
|
||||
expressionType: 'SQL',
|
||||
} as AdhocColumn;
|
||||
temporalColumnAdded = true;
|
||||
return false; // Do not include this in the output; it's added separately
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// So we ensure the temporal column is added first
|
||||
if (temporalColumn) {
|
||||
columns = [temporalColumn, ...columns];
|
||||
}
|
||||
}
|
||||
|
||||
const moreProps: Partial<QueryObject> = {};
|
||||
const ownState = options?.ownState ?? {};
|
||||
// Build Query flag to check if its for either download as csv, excel or json
|
||||
const isDownloadQuery =
|
||||
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
||||
(formData?.result_format === 'json' &&
|
||||
formData?.result_type === 'results');
|
||||
|
||||
if (isDownloadQuery) {
|
||||
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
||||
moreProps.row_offset = 0;
|
||||
}
|
||||
|
||||
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
||||
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
||||
const currentPage = ownState.currentPage ?? 0;
|
||||
|
||||
moreProps.row_limit = pageSize;
|
||||
moreProps.row_offset = currentPage * pageSize;
|
||||
}
|
||||
|
||||
// getting sort by in case of server pagination from own state
|
||||
let sortByFromOwnState: QueryFormOrderBy[] | undefined;
|
||||
if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
|
||||
const sortByItem = ownState?.sortBy[0];
|
||||
sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
|
||||
}
|
||||
|
||||
let queryObject = {
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
extras,
|
||||
orderby:
|
||||
formData.server_pagination && sortByFromOwnState
|
||||
? sortByFromOwnState
|
||||
: orderby,
|
||||
metrics,
|
||||
post_processing: postProcessing,
|
||||
time_offsets: timeOffsets,
|
||||
...moreProps,
|
||||
};
|
||||
|
||||
if (
|
||||
formData.server_pagination &&
|
||||
options?.extras?.cachedChanges?.[formData.slice_id] &&
|
||||
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
|
||||
JSON.stringify(queryObject.filters)
|
||||
) {
|
||||
queryObject = { ...queryObject, row_offset: 0 };
|
||||
const modifiedOwnState = {
|
||||
...(options?.ownState || {}),
|
||||
currentPage: 0,
|
||||
pageSize: queryObject.row_limit ?? 0,
|
||||
};
|
||||
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
||||
}
|
||||
// Because we use same buildQuery for all table on the page we need split them by id
|
||||
options?.hooks?.setCachedChanges({
|
||||
[formData.slice_id]: queryObject.filters,
|
||||
});
|
||||
|
||||
const extraQueries: QueryObject[] = [];
|
||||
if (
|
||||
metrics?.length &&
|
||||
formData.show_totals &&
|
||||
queryMode === QueryMode.Aggregate
|
||||
) {
|
||||
extraQueries.push({
|
||||
...queryObject,
|
||||
columns: [],
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
order_desc: undefined, // we don't need orderby stuff here,
|
||||
orderby: undefined, // because this query will be used for get total aggregation.
|
||||
});
|
||||
}
|
||||
|
||||
const interactiveGroupBy = formData.extra_form_data?.interactive_groupby;
|
||||
if (interactiveGroupBy && queryObject.columns) {
|
||||
queryObject.columns = [
|
||||
...new Set([...queryObject.columns, ...interactiveGroupBy]),
|
||||
];
|
||||
}
|
||||
|
||||
if (formData.server_pagination) {
|
||||
// Add search filter if search text exists
|
||||
if (ownState.searchText && ownState?.searchColumn) {
|
||||
queryObject = {
|
||||
...queryObject,
|
||||
filters: [
|
||||
...(queryObject.filters || []),
|
||||
{
|
||||
col: ownState?.searchColumn,
|
||||
op: 'ILIKE',
|
||||
val: `${ownState.searchText}%`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Now since row limit control is always visible even
|
||||
// in case of server pagination
|
||||
// we must use row limit from form data
|
||||
if (formData.server_pagination && !isDownloadQuery) {
|
||||
return [
|
||||
{ ...queryObject },
|
||||
{
|
||||
...queryObject,
|
||||
time_offsets: [],
|
||||
row_limit: Number(formData?.row_limit) ?? 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
is_rowcount: true,
|
||||
},
|
||||
...extraQueries,
|
||||
];
|
||||
}
|
||||
|
||||
return [queryObject, ...extraQueries];
|
||||
});
|
||||
};
|
||||
|
||||
// Use this closure to cache changing of external filters, if we have server pagination we need reset page to 0, after
|
||||
// external filter changed
|
||||
export const cachedBuildQuery = (): BuildQuery<TableChartFormData> => {
|
||||
let cachedChanges: any = {};
|
||||
const setCachedChanges = (newChanges: any) => {
|
||||
cachedChanges = { ...cachedChanges, ...newChanges };
|
||||
};
|
||||
|
||||
return (formData, options) =>
|
||||
buildQuery(
|
||||
{ ...formData },
|
||||
{
|
||||
extras: { cachedChanges },
|
||||
ownState: options?.ownState ?? {},
|
||||
hooks: {
|
||||
...options?.hooks,
|
||||
setDataMask: () => {},
|
||||
setCachedChanges,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default cachedBuildQuery();
|
||||
Reference in New Issue
Block a user