+ );
+ },
+);
+
+AgGridDataTable.displayName = 'AgGridDataTable';
+
+export default memo(AgGridDataTable);
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx
new file mode 100644
index 00000000000..58273070d7c
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx
@@ -0,0 +1,292 @@
+/**
+ * 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 {
+ DataRecord,
+ DataRecordValue,
+ GenericDataType,
+ getTimeFormatterForGranularity,
+ t,
+} from '@superset-ui/core';
+import { useCallback, useEffect, useState, useMemo } from 'react';
+import { isEqual } from 'lodash';
+
+import { CellClickedEvent, IMenuActionParams } from 'ag-grid-community';
+import {
+ AgGridTableChartTransformedProps,
+ InputColumn,
+ SearchOption,
+ SortByItem,
+} from './types';
+import AgGridDataTable from './AgGridTable';
+import { updateTableOwnState } from './utils/externalAPIs';
+import TimeComparisonVisibility from './AgGridTable/components/TimeComparisonVisibility';
+import { useColDefs } from './utils/useColDefs';
+import { getCrossFilterDataMask } from './utils/getCrossFilterDataMask';
+import { StyledChartContainer } from './styles';
+
+const getGridHeight = (height: number, includeSearch: boolean | undefined) => {
+ let calculatedGridHeight = height;
+ if (includeSearch) {
+ calculatedGridHeight -= 16;
+ }
+ return calculatedGridHeight - 80;
+};
+
+export default function TableChart(
+ props: AgGridTableChartTransformedProps & {},
+) {
+ const {
+ height,
+ columns,
+ data,
+ includeSearch,
+ allowRearrangeColumns,
+ pageSize,
+ serverPagination,
+ rowCount,
+ setDataMask,
+ serverPaginationData,
+ slice_id,
+ percentMetrics,
+ hasServerPageLengthChanged,
+ serverPageLength,
+ emitCrossFilters,
+ filters,
+ timeGrain,
+ isRawRecords,
+ alignPositiveNegative,
+ showCellBars,
+ isUsingTimeComparison,
+ colorPositiveNegative,
+ totals,
+ showTotals,
+ columnColorFormatters,
+ basicColorFormatters,
+ width,
+ } = props;
+
+ const [searchOptions, setSearchOptions] = useState([]);
+
+ useEffect(() => {
+ const options = columns
+ .filter(col => col?.dataType === GenericDataType.String)
+ .map(column => ({
+ value: column.key,
+ label: column.label,
+ }));
+
+ if (!isEqual(options, searchOptions)) {
+ setSearchOptions(options || []);
+ }
+ }, [columns]);
+
+ const comparisonColumns = [
+ { key: 'all', label: t('Display all') },
+ { key: '#', label: '#' },
+ { key: '△', label: '△' },
+ { key: '%', label: '%' },
+ ];
+
+ const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
+ comparisonColumns?.[0]?.key,
+ ]);
+
+ const filteredColumns = useMemo(() => {
+ if (!isUsingTimeComparison) {
+ return columns;
+ }
+ if (
+ selectedComparisonColumns.length === 0 ||
+ selectedComparisonColumns.includes('all')
+ ) {
+ return columns?.filter(col => col?.config?.visible !== false);
+ }
+
+ return columns
+ .filter(
+ col =>
+ !col.originalLabel ||
+ (col?.label || '').includes('Main') ||
+ selectedComparisonColumns.includes(col.label),
+ )
+ .filter(col => col?.config?.visible !== false);
+ }, [columns, selectedComparisonColumns]);
+
+ const colDefs = useColDefs({
+ columns: isUsingTimeComparison
+ ? (filteredColumns as InputColumn[])
+ : (columns as InputColumn[]),
+ data,
+ serverPagination,
+ isRawRecords,
+ defaultAlignPN: alignPositiveNegative,
+ showCellBars,
+ colorPositiveNegative,
+ totals,
+ columnColorFormatters,
+ allowRearrangeColumns,
+ basicColorFormatters,
+ isUsingTimeComparison,
+ emitCrossFilters,
+ alignPositiveNegative,
+ slice_id,
+ });
+
+ const gridHeight = getGridHeight(height, includeSearch);
+
+ const isActiveFilterValue = useCallback(
+ function isActiveFilterValue(key: string, val: DataRecordValue) {
+ return !!filters && filters[key]?.includes(val);
+ },
+ [filters],
+ );
+
+ const timestampFormatter = useCallback(
+ value => getTimeFormatterForGranularity(timeGrain)(value),
+ [timeGrain],
+ );
+
+ const toggleFilter = useCallback(
+ (event: CellClickedEvent | IMenuActionParams) => {
+ if (
+ emitCrossFilters &&
+ event.column &&
+ !(
+ event.column.getColDef().context?.isMetric ||
+ event.column.getColDef().context?.isPercentMetric
+ )
+ ) {
+ const crossFilterProps = {
+ key: event.column.getColId(),
+ value: event.value,
+ filters,
+ timeGrain,
+ isActiveFilterValue,
+ timestampFormatter,
+ };
+ setDataMask(getCrossFilterDataMask(crossFilterProps).dataMask);
+ }
+ },
+ [emitCrossFilters, setDataMask, filters, timeGrain],
+ );
+
+ const handleServerPaginationChange = useCallback(
+ (pageNumber: number, pageSize: number) => {
+ const modifiedOwnState = {
+ ...serverPaginationData,
+ currentPage: pageNumber,
+ pageSize,
+ };
+ updateTableOwnState(setDataMask, modifiedOwnState);
+ },
+ [setDataMask],
+ );
+
+ const handlePageSizeChange = useCallback(
+ (pageSize: number) => {
+ const modifiedOwnState = {
+ ...serverPaginationData,
+ currentPage: 0,
+ pageSize,
+ };
+ updateTableOwnState(setDataMask, modifiedOwnState);
+ },
+ [setDataMask],
+ );
+
+ const handleChangeSearchCol = (searchCol: string) => {
+ if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
+ const modifiedOwnState = {
+ ...(serverPaginationData || {}),
+ searchColumn: searchCol,
+ searchText: '',
+ };
+ updateTableOwnState(setDataMask, modifiedOwnState);
+ }
+ };
+
+ const handleSearch = useCallback(
+ (searchText: string) => {
+ const modifiedOwnState = {
+ ...(serverPaginationData || {}),
+ searchColumn:
+ serverPaginationData?.searchColumn || searchOptions[0]?.value,
+ searchText,
+ currentPage: 0, // Reset to first page when searching
+ };
+ updateTableOwnState(setDataMask, modifiedOwnState);
+ },
+ [setDataMask, searchOptions],
+ );
+
+ const handleSortByChange = useCallback(
+ (sortBy: SortByItem[]) => {
+ if (!serverPagination) return;
+ const modifiedOwnState = {
+ ...serverPaginationData,
+ sortBy,
+ };
+ updateTableOwnState(setDataMask, modifiedOwnState);
+ },
+ [setDataMask, serverPagination],
+ );
+
+ const renderTimeComparisonVisibility = (): JSX.Element => (
+
+ );
+
+ return (
+
+ null
+ }
+ cleanedTotals={totals || {}}
+ showTotals={showTotals}
+ width={width}
+ />
+
+ );
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts
new file mode 100644
index 00000000000..64b091e87fd
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts
@@ -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 = (
+ 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((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 = {};
+ 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 => {
+ 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();
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts
new file mode 100644
index 00000000000..d4db087c304
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/consts.ts
@@ -0,0 +1,33 @@
+/**
+ * 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 { formatSelectOptions } from '@superset-ui/chart-controls';
+import { addLocaleData } from '@superset-ui/core';
+import i18n from './i18n';
+
+addLocaleData(i18n);
+
+export const SERVER_PAGE_SIZE_OPTIONS = formatSelectOptions([
+ 10, 20, 50, 100, 200,
+]);
+
+export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200];
+
+export const CUSTOM_AGG_FUNCS = {
+ queryTotal: 'Metric total',
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
new file mode 100644
index 00000000000..173e8f1828e
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
@@ -0,0 +1,753 @@
+/* eslint-disable camelcase */
+/**
+ * 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 {
+ ColumnMeta,
+ ColumnOption,
+ ControlConfig,
+ ControlPanelConfig,
+ ControlPanelsContainerProps,
+ ControlPanelState,
+ ControlState,
+ ControlStateMapping,
+ D3_TIME_FORMAT_OPTIONS,
+ Dataset,
+ DEFAULT_MAX_ROW,
+ DEFAULT_MAX_ROW_TABLE_SERVER,
+ defineSavedMetrics,
+ formatSelectOptions,
+ getStandardizedControls,
+ QueryModeLabel,
+ sections,
+ sharedControls,
+} from '@superset-ui/chart-controls';
+import {
+ ensureIsArray,
+ FeatureFlag,
+ GenericDataType,
+ getMetricLabel,
+ isAdhocColumn,
+ isFeatureEnabled,
+ isPhysicalColumn,
+ legacyValidateInteger,
+ QueryFormColumn,
+ QueryFormMetric,
+ QueryMode,
+ SMART_DATE_ID,
+ t,
+ validateMaxValue,
+ validateServerPagination,
+} from '@superset-ui/core';
+
+import { isEmpty, last } from 'lodash';
+import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
+import { ColorSchemeEnum } from './types';
+
+/**
+ * Generate comparison column names for a given column.
+ */
+const generateComparisonColumns = (colname: string) => [
+ `${t('Main')} ${colname}`,
+ `# ${colname}`,
+ `△ ${colname}`,
+ `% ${colname}`,
+];
+
+/**
+ * Generate column types for the comparison columns.
+ */
+const generateComparisonColumnTypes = (count: number) =>
+ Array(count).fill(GenericDataType.Numeric);
+
+function getQueryMode(controls: ControlStateMapping): QueryMode {
+ const mode = controls?.query_mode?.value;
+ if (mode === QueryMode.Aggregate || mode === QueryMode.Raw) {
+ return mode as QueryMode;
+ }
+ const rawColumns = controls?.all_columns?.value as
+ | QueryFormColumn[]
+ | undefined;
+ const hasRawColumns = rawColumns && rawColumns.length > 0;
+ return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
+}
+
+const processComparisonColumns = (columns: any[], suffix: string) =>
+ columns
+ .map(col => {
+ if (!col.label.includes(suffix)) {
+ return [
+ {
+ label: `${t('Main')} ${col.label}`,
+ value: `${t('Main')} ${col.value}`,
+ },
+ {
+ label: `# ${col.label}`,
+ value: `# ${col.value}`,
+ },
+ {
+ label: `△ ${col.label}`,
+ value: `△ ${col.value}`,
+ },
+ {
+ label: `% ${col.label}`,
+ value: `% ${col.value}`,
+ },
+ ];
+ }
+ return [];
+ })
+ .flat();
+
+/**
+ * Visibility check
+ */
+function isQueryMode(mode: QueryMode) {
+ return ({ controls }: Pick) =>
+ getQueryMode(controls) === mode;
+}
+
+const isAggMode = isQueryMode(QueryMode.Aggregate);
+const isRawMode = isQueryMode(QueryMode.Raw);
+
+const validateAggControlValues = (
+ controls: ControlStateMapping,
+ values: any[],
+) => {
+ const areControlsEmpty = values.every(val => ensureIsArray(val).length === 0);
+ return areControlsEmpty && isAggMode({ controls })
+ ? [t('Group By, Metrics or Percentage Metrics must have a value')]
+ : [];
+};
+
+const queryMode: ControlConfig<'RadioButtonControl'> = {
+ type: 'RadioButtonControl',
+ label: t('Query mode'),
+ default: null,
+ options: [
+ [QueryMode.Aggregate, QueryModeLabel[QueryMode.Aggregate]],
+ [QueryMode.Raw, QueryModeLabel[QueryMode.Raw]],
+ ],
+ mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }),
+ rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'],
+};
+
+const allColumnsControl: typeof sharedControls.groupby = {
+ ...sharedControls.groupby,
+ label: t('Columns'),
+ description: t('Columns to display'),
+ multi: true,
+ freeForm: true,
+ allowAll: true,
+ commaChoosesOption: false,
+ optionRenderer: c => ,
+ valueRenderer: c => ,
+ valueKey: 'column_name',
+ mapStateToProps: ({ datasource, controls }, controlState) => ({
+ options: datasource?.columns || [],
+ queryMode: getQueryMode(controls),
+ externalValidationErrors:
+ isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0
+ ? [t('must have a value')]
+ : [],
+ }),
+ visibility: isRawMode,
+ resetOnHide: false,
+};
+
+const percentMetricsControl: typeof sharedControls.metrics = {
+ ...sharedControls.metrics,
+ label: t('Percentage metrics'),
+ description: t(
+ 'Select one or many metrics to display, that will be displayed in the percentages of total. ' +
+ 'Percentage metrics will be calculated only from data within the row limit. ' +
+ 'You can use an aggregation function on a column or write custom SQL to create a percentage metric.',
+ ),
+ visibility: isAggMode,
+ resetOnHide: false,
+ mapStateToProps: ({ datasource, controls }, controlState) => ({
+ columns: datasource?.columns || [],
+ savedMetrics: defineSavedMetrics(datasource),
+ datasource,
+ datasourceType: datasource?.type,
+ queryMode: getQueryMode(controls),
+ externalValidationErrors: validateAggControlValues(controls, [
+ controls.groupby?.value,
+ controls.metrics?.value,
+ controlState?.value,
+ ]),
+ }),
+ rerender: ['groupby', 'metrics'],
+ default: [],
+ validators: [],
+};
+
+/*
+Options for row limit control
+*/
+
+export const ROW_LIMIT_OPTIONS_TABLE = [
+ 10, 50, 100, 250, 500, 1000, 5000, 10000, 50000, 100000, 150000, 200000,
+ 250000, 300000, 350000, 400000, 450000, 500000,
+];
+
+const config: ControlPanelConfig = {
+ controlPanelSections: [
+ {
+ label: t('Query'),
+ expanded: true,
+ controlSetRows: [
+ [
+ {
+ name: 'query_mode',
+ config: queryMode,
+ },
+ ],
+ [
+ {
+ name: 'groupby',
+ override: {
+ visibility: isAggMode,
+ resetOnHide: false,
+ mapStateToProps: (
+ state: ControlPanelState,
+ controlState: ControlState,
+ ) => {
+ const { controls } = state;
+ const originalMapStateToProps =
+ sharedControls?.groupby?.mapStateToProps;
+ const newState =
+ originalMapStateToProps?.(state, controlState) ?? {};
+ newState.externalValidationErrors = validateAggControlValues(
+ controls,
+ [controls.metrics?.value, controlState.value],
+ );
+
+ return newState;
+ },
+ rerender: ['metrics', 'percent_metrics'],
+ },
+ },
+ ],
+ [
+ {
+ name: 'time_grain_sqla',
+ config: {
+ ...sharedControls.time_grain_sqla,
+ visibility: ({ controls }) => {
+ const dttmLookup = Object.fromEntries(
+ ensureIsArray(controls?.groupby?.options).map(option => [
+ option.column_name,
+ option.is_dttm,
+ ]),
+ );
+
+ return ensureIsArray(controls?.groupby.value)
+ .map(selection => {
+ if (isAdhocColumn(selection)) {
+ return true;
+ }
+ if (isPhysicalColumn(selection)) {
+ return !!dttmLookup[selection];
+ }
+ return false;
+ })
+ .some(Boolean);
+ },
+ },
+ },
+ 'temporal_columns_lookup',
+ ],
+ [
+ {
+ name: 'metrics',
+ override: {
+ validators: [],
+ visibility: isAggMode,
+ resetOnHide: false,
+ mapStateToProps: (
+ { controls, datasource, form_data }: ControlPanelState,
+ controlState: ControlState,
+ ) => ({
+ columns: datasource?.columns[0]?.hasOwnProperty('filterable')
+ ? (datasource as Dataset)?.columns?.filter(
+ (c: ColumnMeta) => c.filterable,
+ )
+ : datasource?.columns,
+ savedMetrics: defineSavedMetrics(datasource),
+ // current active adhoc metrics
+ selectedMetrics:
+ form_data.metrics ||
+ (form_data.metric ? [form_data.metric] : []),
+ datasource,
+ externalValidationErrors: validateAggControlValues(controls, [
+ controls.groupby?.value,
+ controlState.value,
+ ]),
+ }),
+ rerender: ['groupby'],
+ },
+ },
+ {
+ name: 'all_columns',
+ config: allColumnsControl,
+ },
+ ],
+ [
+ {
+ name: 'percent_metrics',
+ config: percentMetricsControl,
+ },
+ ],
+ ['adhoc_filters'],
+ [
+ {
+ name: 'timeseries_limit_metric',
+ override: {
+ visibility: isAggMode,
+ resetOnHide: false,
+ },
+ },
+ {
+ name: 'order_by_cols',
+ config: {
+ type: 'SelectControl',
+ label: t('Ordering'),
+ description: t('Order results by selected columns'),
+ multi: true,
+ default: [],
+ mapStateToProps: ({ datasource }) => ({
+ choices: datasource?.hasOwnProperty('order_by_choices')
+ ? (datasource as Dataset)?.order_by_choices
+ : datasource?.columns || [],
+ }),
+ visibility: isRawMode,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'order_desc',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Sort descending'),
+ default: true,
+ description: t(
+ 'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
+ ),
+ visibility: ({ controls }: ControlPanelsContainerProps) => {
+ const hasSortMetric = Boolean(
+ controls?.timeseries_limit_metric?.value,
+ );
+ return hasSortMetric && isAggMode({ controls });
+ },
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'server_pagination',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Server pagination'),
+ description: t(
+ 'Enable server side pagination of results (experimental feature)',
+ ),
+ default: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'server_page_length',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Server Page Length'),
+ default: 10,
+ choices: SERVER_PAGE_SIZE_OPTIONS,
+ description: t('Rows per page, 0 means no pagination'),
+ visibility: ({ controls }: ControlPanelsContainerProps) =>
+ Boolean(controls?.server_pagination?.value),
+ },
+ },
+ ],
+ [
+ {
+ name: 'row_limit',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Row limit'),
+ clearable: false,
+ mapStateToProps: state => ({
+ maxValue: state?.common?.conf?.TABLE_VIZ_MAX_ROW_SERVER,
+ server_pagination: state?.form_data?.server_pagination,
+ maxValueWithoutServerPagination:
+ state?.common?.conf?.SQL_MAX_ROW,
+ }),
+ validators: [
+ legacyValidateInteger,
+ (v, state) =>
+ validateMaxValue(
+ v,
+ state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
+ ),
+ (v, state) =>
+ validateServerPagination(
+ v,
+ state?.server_pagination,
+ state?.maxValueWithoutServerPagination || DEFAULT_MAX_ROW,
+ state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
+ ),
+ ],
+ // Re run the validations when this control value
+ validationDependancies: ['server_pagination'],
+ default: 10000,
+ choices: formatSelectOptions(ROW_LIMIT_OPTIONS_TABLE),
+ description: t(
+ 'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
+ ),
+ },
+ override: {
+ default: 1000,
+ },
+ },
+ ],
+ [
+ {
+ name: 'show_totals',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Show summary'),
+ default: false,
+ description: t(
+ 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
+ ),
+ visibility: isAggMode,
+ resetOnHide: false,
+ },
+ },
+ ],
+ ],
+ },
+ {
+ label: t('Options'),
+ expanded: true,
+ controlSetRows: [
+ [
+ {
+ name: 'table_timestamp_format',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Timestamp format'),
+ default: SMART_DATE_ID,
+ renderTrigger: true,
+ clearable: false,
+ choices: D3_TIME_FORMAT_OPTIONS,
+ description: t('D3 time format for datetime columns'),
+ },
+ },
+ ],
+ [
+ {
+ name: 'page_length',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ renderTrigger: true,
+ label: t('Page length'),
+ default: null,
+ choices: PAGE_SIZE_OPTIONS,
+ description: t('Rows per page, 0 means no pagination'),
+ visibility: ({ controls }: ControlPanelsContainerProps) =>
+ !controls?.server_pagination?.value,
+ },
+ },
+ null,
+ ],
+ [
+ {
+ name: 'include_search',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Search box'),
+ renderTrigger: true,
+ default: false,
+ description: t('Whether to include a client-side search box'),
+ },
+ },
+ ],
+ [
+ {
+ name: 'column_config',
+ config: {
+ type: 'ColumnConfigControl',
+ label: t('Customize columns'),
+ description: t('Further customize how to display each column'),
+ width: 400,
+ height: 320,
+ renderTrigger: true,
+ shouldMapStateToProps() {
+ return true;
+ },
+ mapStateToProps(explore, _, chart) {
+ const timeComparisonValue =
+ explore?.controls?.time_compare?.value;
+ const { colnames: _colnames, coltypes: _coltypes } =
+ chart?.queriesResponse?.[0] ?? {};
+ let colnames: string[] = _colnames || [];
+ let coltypes: GenericDataType[] = _coltypes || [];
+ const childColumnMap: Record = {};
+ const timeComparisonColumnMap: Record = {};
+
+ if (!isEmpty(timeComparisonValue)) {
+ /**
+ * Replace numeric columns with sets of comparison columns.
+ */
+ const updatedColnames: string[] = [];
+ const updatedColtypes: GenericDataType[] = [];
+
+ colnames
+ .filter(
+ colname =>
+ last(colname.split('__')) !== timeComparisonValue,
+ )
+ .forEach((colname, index) => {
+ if (
+ explore.form_data.metrics?.some(
+ metric => getMetricLabel(metric) === colname,
+ ) ||
+ explore.form_data.percent_metrics?.some(
+ (metric: QueryFormMetric) =>
+ getMetricLabel(metric) === colname,
+ )
+ ) {
+ const comparisonColumns =
+ generateComparisonColumns(colname);
+ comparisonColumns.forEach((name, idx) => {
+ updatedColnames.push(name);
+ updatedColtypes.push(
+ ...generateComparisonColumnTypes(4),
+ );
+ timeComparisonColumnMap[name] = true;
+ if (idx === 0 && name.startsWith('Main ')) {
+ childColumnMap[name] = false;
+ } else {
+ childColumnMap[name] = true;
+ }
+ });
+ } else {
+ updatedColnames.push(colname);
+ updatedColtypes.push(coltypes[index]);
+ childColumnMap[colname] = false;
+ timeComparisonColumnMap[colname] = false;
+ }
+ });
+
+ colnames = updatedColnames;
+ coltypes = updatedColtypes;
+ }
+ return {
+ columnsPropsObject: {
+ colnames,
+ coltypes,
+ childColumnMap,
+ timeComparisonColumnMap,
+ },
+ };
+ },
+ },
+ },
+ ],
+ ],
+ },
+ {
+ label: t('Visual formatting'),
+ expanded: true,
+ controlSetRows: [
+ [
+ {
+ name: 'show_cell_bars',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Show Cell bars'),
+ renderTrigger: true,
+ default: true,
+ description: t(
+ 'Whether to display a bar chart background in table columns',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'align_pn',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Align +/-'),
+ renderTrigger: true,
+ default: false,
+ description: t(
+ 'Whether to align background charts with both positive and negative values at 0',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'color_pn',
+ config: {
+ type: 'CheckboxControl',
+ label: t('add colors to cell bars for +/-'),
+ renderTrigger: true,
+ default: true,
+ description: t(
+ 'Whether to colorize numeric values by whether they are positive or negative',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'comparison_color_enabled',
+ config: {
+ type: 'CheckboxControl',
+ label: t('basic conditional formatting'),
+ renderTrigger: true,
+ visibility: ({ controls }) =>
+ !isEmpty(controls?.time_compare?.value),
+ default: false,
+ description: t(
+ 'This will be applied to the whole table. Arrows (↑ and ↓) will be added to ' +
+ 'main columns for increase and decrease. Basic conditional formatting can be ' +
+ 'overwritten by conditional formatting below.',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'comparison_color_scheme',
+ config: {
+ type: 'SelectControl',
+ label: t('color type'),
+ default: ColorSchemeEnum.Green,
+ renderTrigger: true,
+ choices: [
+ [ColorSchemeEnum.Green, 'Green for increase, red for decrease'],
+ [ColorSchemeEnum.Red, 'Red for increase, green for decrease'],
+ ],
+ visibility: ({ controls }) =>
+ !isEmpty(controls?.time_compare?.value) &&
+ Boolean(controls?.comparison_color_enabled?.value),
+ description: t(
+ 'Adds color to the chart symbols based on the positive or ' +
+ 'negative change from the comparison value.',
+ ),
+ },
+ },
+ ],
+ [
+ {
+ name: 'conditional_formatting',
+ config: {
+ type: 'ConditionalFormattingControl',
+ renderTrigger: true,
+ label: t('Custom Conditional Formatting'),
+ extraColorChoices: [
+ {
+ value: ColorSchemeEnum.Green,
+ label: t('Green for increase, red for decrease'),
+ },
+ {
+ value: ColorSchemeEnum.Red,
+ label: t('Red for increase, green for decrease'),
+ },
+ ],
+ description: t(
+ 'Apply conditional color formatting to numeric columns',
+ ),
+ shouldMapStateToProps() {
+ return true;
+ },
+ mapStateToProps(explore, _, chart) {
+ const verboseMap = explore?.datasource?.hasOwnProperty(
+ 'verbose_map',
+ )
+ ? (explore?.datasource as Dataset)?.verbose_map
+ : (explore?.datasource?.columns ?? {});
+ const chartStatus = chart?.chartStatus;
+ const { colnames, coltypes } =
+ chart?.queriesResponse?.[0] ?? {};
+ const numericColumns =
+ Array.isArray(colnames) && Array.isArray(coltypes)
+ ? colnames
+ .filter(
+ (colname: string, index: number) =>
+ coltypes[index] === GenericDataType.Numeric,
+ )
+ .map((colname: string) => ({
+ value: colname,
+ label: Array.isArray(verboseMap)
+ ? colname
+ : (verboseMap[colname] ?? colname),
+ }))
+ : [];
+ const columnOptions = explore?.controls?.time_compare?.value
+ ? processComparisonColumns(
+ numericColumns || [],
+ ensureIsArray(
+ explore?.controls?.time_compare?.value,
+ )[0]?.toString() || '',
+ )
+ : numericColumns;
+
+ return {
+ removeIrrelevantConditions: chartStatus === 'success',
+ columnOptions,
+ verboseMap,
+ };
+ },
+ },
+ },
+ ],
+ ],
+ },
+ {
+ ...sections.timeComparisonControls({
+ multi: false,
+ showCalculationType: false,
+ showFullChoices: false,
+ }),
+ visibility: ({ controls }) =>
+ isAggMode({ controls }) &&
+ isFeatureEnabled(FeatureFlag.TableV2TimeComparisonEnabled),
+ },
+ ],
+ formDataOverrides: formData => ({
+ ...formData,
+ metrics: getStandardizedControls().popAllMetrics(),
+ groupby: getStandardizedControls().popAllColumns(),
+ }),
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/i18n.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/i18n.ts
new file mode 100644
index 00000000000..3ea82b00e07
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/i18n.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { Locale } from '@superset-ui/core';
+
+const en = {
+ 'Query Mode': [''],
+ Aggregate: [''],
+ 'Raw Records': [''],
+ 'Emit Filter Events': [''],
+ 'Show Cell Bars': [''],
+ 'page_size.show': ['Show'],
+ 'page_size.all': ['All'],
+ 'page_size.entries': ['entries'],
+ 'table.previous_page': ['Previous'],
+ 'table.next_page': ['Next'],
+ 'search.num_records': ['%s record', '%s records...'],
+};
+
+const translations: Partial> = {
+ en,
+ fr: {
+ 'Query Mode': [''],
+ Aggregate: [''],
+ 'Raw Records': [''],
+ 'Emit Filter Events': [''],
+ 'Show Cell Bars': [''],
+ 'page_size.show': ['Afficher'],
+ 'page_size.all': ['tous'],
+ 'page_size.entries': ['entrées'],
+ 'table.previous_page': ['Précédent'],
+ 'table.next_page': ['Suivante'],
+ 'search.num_records': ['%s enregistrement', '%s enregistrements...'],
+ },
+ zh: {
+ 'Query Mode': ['查询模式'],
+ Aggregate: ['分组聚合'],
+ 'Raw Records': ['原始数据'],
+ 'Emit Filter Events': ['关联看板过滤器'],
+ 'Show Cell Bars': ['为指标添加条状图背景'],
+ 'page_size.show': ['每页显示'],
+ 'page_size.all': ['全部'],
+ 'page_size.entries': ['条'],
+ 'table.previous_page': ['上一页'],
+ 'table.next_page': ['下一页'],
+ 'search.num_records': ['%s条记录...'],
+ },
+};
+
+export default translations;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table.jpg b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table.jpg
new file mode 100644
index 00000000000..431a63a9632
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table.jpg differ
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table2.jpg b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table2.jpg
new file mode 100644
index 00000000000..0ec74d54a1b
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table2.jpg differ
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table3.jpg b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table3.jpg
new file mode 100644
index 00000000000..532aeeeed7d
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/Table3.jpg differ
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png
new file mode 100644
index 00000000000..296702c954a
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png differ
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnailLarge.png
new file mode 100644
index 00000000000..133d2804f84
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/images/thumbnailLarge.png differ
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/index.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/index.ts
new file mode 100644
index 00000000000..a5650637d5b
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/index.ts
@@ -0,0 +1,71 @@
+/**
+ * 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
+import example1 from './images/Table.jpg';
+import example2 from './images/Table2.jpg';
+import example3 from './images/Table3.jpg';
+import controlPanel from './controlPanel';
+import buildQuery from './buildQuery';
+import { TableChartFormData, TableChartProps } from './types';
+
+// must export something for the module to be exist in dev mode
+export { default as __hack__ } from './types';
+export * from './types';
+
+const metadata = new ChartMetadata({
+ behaviors: [
+ Behavior.InteractiveChart,
+ Behavior.DrillToDetail,
+ Behavior.DrillBy,
+ ],
+ category: t('Table'),
+ canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
+ description: t(
+ 'Classic row-by-column spreadsheet like view of a dataset. Use tables to showcase a view into the underlying data or to show aggregated metrics.',
+ ),
+ exampleGallery: [{ url: example1 }, { url: example2 }, { url: example3 }],
+ name: t('Table V2'),
+ tags: [
+ t('Additive'),
+ t('Business'),
+ t('Pattern'),
+ t('Featured'),
+ t('Report'),
+ t('Sequential'),
+ t('Tabular'),
+ ],
+ thumbnail,
+});
+
+export default class AgGridTableChartPlugin extends ChartPlugin<
+ TableChartFormData,
+ TableChartProps
+> {
+ constructor() {
+ super({
+ loadChart: () => import('./AgGridTableChart'),
+ metadata,
+ transformProps,
+ controlPanel,
+ buildQuery,
+ });
+ }
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx
new file mode 100644
index 00000000000..450a5fbd2e5
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/NumericCellRenderer.tsx
@@ -0,0 +1,206 @@
+/**
+ * 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 { styled } from '@superset-ui/core';
+import { CustomCellRendererProps } from 'ag-grid-react';
+import { BasicColorFormatterType, InputColumn } from '../types';
+import { useIsDark } from '../utils/useTableTheme';
+
+const StyledTotalCell = styled.div`
+ ${() => `
+ font-weight: bold;
+ `}
+`;
+
+const CellContainer = styled.div<{ backgroundColor?: string; align?: string }>`
+ display: flex;
+ background-color: ${({ backgroundColor }) =>
+ backgroundColor || 'transparent'};
+ justify-content: ${({ align }) => align || 'left'};
+`;
+
+const ArrowContainer = styled.div<{ arrowColor?: string }>`
+ margin-right: 10px;
+ color: ${({ arrowColor }) => arrowColor || 'inherit'};
+`;
+
+const Bar = styled.div<{
+ offset: number;
+ percentage: number;
+ background: string;
+}>`
+ position: absolute;
+ left: ${({ offset }) => `${offset}%`};
+ top: 0;
+ height: 100%;
+ width: ${({ percentage }) => `${percentage}%`};
+ background-color: ${({ background }) => background};
+ z-index: 1;
+`;
+
+type ValueRange = [number, number];
+
+/**
+ * Cell background width calculation for horizontal bar chart
+ */
+function cellWidth({
+ value,
+ valueRange,
+ alignPositiveNegative,
+}: {
+ value: number;
+ valueRange: ValueRange;
+ alignPositiveNegative: boolean;
+}) {
+ const [minValue, maxValue] = valueRange;
+ if (alignPositiveNegative) {
+ const perc = Math.abs(Math.round((value / maxValue) * 100));
+ return perc;
+ }
+ const posExtent = Math.abs(Math.max(maxValue, 0));
+ const negExtent = Math.abs(Math.min(minValue, 0));
+ const tot = posExtent + negExtent;
+ const perc2 = Math.round((Math.abs(value) / tot) * 100);
+ return perc2;
+}
+
+/**
+ * Cell left margin (offset) calculation for horizontal bar chart elements
+ * when alignPositiveNegative is not set
+ */
+function cellOffset({
+ value,
+ valueRange,
+ alignPositiveNegative,
+}: {
+ value: number;
+ valueRange: ValueRange;
+ alignPositiveNegative: boolean;
+}) {
+ if (alignPositiveNegative) {
+ return 0;
+ }
+ const [minValue, maxValue] = valueRange;
+ const posExtent = Math.abs(Math.max(maxValue, 0));
+ const negExtent = Math.abs(Math.min(minValue, 0));
+ const tot = posExtent + negExtent;
+ return Math.round((Math.min(negExtent + value, negExtent) / tot) * 100);
+}
+
+/**
+ * Cell background color calculation for horizontal bar chart
+ */
+function cellBackground({
+ value,
+ colorPositiveNegative = false,
+ isDarkTheme = false,
+}: {
+ value: number;
+ colorPositiveNegative: boolean;
+ isDarkTheme: boolean;
+}) {
+ if (!colorPositiveNegative) {
+ return isDarkTheme ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'; // transparent or neutral
+ }
+
+ const r = value < 0 ? 150 : 0;
+ const g = value >= 0 ? 150 : 0;
+ return `rgba(${r},${g},0,0.2)`;
+}
+
+export const NumericCellRenderer = (
+ params: CustomCellRendererProps & {
+ allowRenderHtml: boolean;
+ columns: InputColumn[];
+ hasBasicColorFormatters: boolean | undefined;
+ col: InputColumn;
+ basicColorFormatters: {
+ [Key: string]: BasicColorFormatterType;
+ }[];
+ valueRange: any;
+ alignPositiveNegative: boolean;
+ colorPositiveNegative: boolean;
+ },
+) => {
+ const {
+ value,
+ valueFormatted,
+ node,
+ hasBasicColorFormatters,
+ col,
+ basicColorFormatters,
+ valueRange,
+ alignPositiveNegative,
+ colorPositiveNegative,
+ } = params;
+
+ const isDarkTheme = useIsDark();
+
+ if (node?.rowPinned === 'bottom') {
+ return {valueFormatted ?? value};
+ }
+
+ let arrow = '';
+ let arrowColor = '';
+ if (hasBasicColorFormatters && col?.metricName) {
+ arrow =
+ basicColorFormatters?.[node?.rowIndex as number]?.[col.metricName]
+ ?.mainArrow;
+ arrowColor =
+ basicColorFormatters?.[node?.rowIndex as number]?.[
+ col.metricName
+ ]?.arrowColor?.toLowerCase();
+ }
+
+ const alignment =
+ col?.config?.horizontalAlign || (col?.isNumeric ? 'right' : 'left');
+
+ if (!valueRange) {
+ return (
+
+ {arrow && (
+ {arrow}
+ )}
+
{valueFormatted ?? value}
+
+ );
+ }
+
+ const CellWidth = cellWidth({
+ value: value as number,
+ valueRange,
+ alignPositiveNegative,
+ });
+ const CellOffset = cellOffset({
+ value: value as number,
+ valueRange,
+ alignPositiveNegative,
+ });
+ const background = cellBackground({
+ value: value as number,
+ colorPositiveNegative,
+ isDarkTheme,
+ });
+
+ return (
+
+
+ {valueFormatted ?? value}
+
+ );
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx
new file mode 100644
index 00000000000..d71d1ee1bc8
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/renderers/TextCellRenderer.tsx
@@ -0,0 +1,71 @@
+/* eslint-disable camelcase */
+/**
+ * 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 { isProbablyHTML, sanitizeHtml, t } from '@superset-ui/core';
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Tooltip } from '@superset-ui/core/components';
+import { CellRendererProps } from '../types';
+import { SummaryContainer, SummaryText } from '../styles';
+
+const SUMMARY_TOOLTIP_TEXT = t(
+ 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
+);
+
+export const TextCellRenderer = (params: CellRendererProps) => {
+ const { node, api, colDef, columns, allowRenderHtml, value, valueFormatted } =
+ params;
+
+ if (node?.rowPinned === 'bottom') {
+ const cols = api.getAllGridColumns().filter(col => col.isVisible());
+ const colAggCheck = !cols[0].getAggFunc();
+ if (cols.length > 1 && colAggCheck && columns[0].key === colDef?.field) {
+ return (
+
+ {t('Summary')}
+
+
+
+
+ );
+ }
+ if (!value) {
+ return null;
+ }
+ }
+
+ if (!(typeof value === 'string' || value instanceof Date)) {
+ return valueFormatted ?? value;
+ }
+
+ if (typeof value === 'string') {
+ if (value.startsWith('http://') || value.startsWith('https://')) {
+ return (
+
+ {value}
+
+ );
+ }
+ if (allowRenderHtml && isProbablyHTML(value)) {
+ return ;
+ }
+ }
+
+ return
{valueFormatted ?? value}
;
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/styles/index.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/styles/index.tsx
new file mode 100644
index 00000000000..a80ff873613
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/styles/index.tsx
@@ -0,0 +1,405 @@
+/**
+ * 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 { css, styled } from '@superset-ui/core';
+import { Select } from '@superset-ui/core/components';
+
+/* Components for AgGridTable */
+// Header Styles
+export const Container = styled.div`
+ ${({ theme }) => `
+ display: flex;
+ width: 100%;
+
+ .three-dots-menu {
+ align-self: center;
+ margin-left: ${theme.sizeUnit}px;
+ cursor: pointer;
+ padding: ${theme.sizeUnit / 2}px;
+ border-radius: ${theme.borderRadius}px;
+ margin-top: ${theme.sizeUnit * 0.75}px;
+ }
+ `}
+`;
+
+export const HeaderContainer = styled.div`
+ ${({ theme }) => `
+ width: 100%;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ padding: 0 ${theme.sizeUnit * 2}px;
+ overflow: hidden;
+ `}
+`;
+
+export const HeaderLabel = styled.span`
+ ${({ theme }) => `
+ font-weight: ${theme.fontWeightStrong};
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: block;
+ max-width: 100%;
+ `}
+`;
+
+export const SortIconWrapper = styled.div`
+ ${({ theme }) => `
+ display: flex;
+ align-items: center;
+ margin-left: ${theme.sizeUnit * 2}px;
+ `}
+`;
+
+export const FilterIconWrapper = styled.div<{ isFilterActive?: boolean }>`
+ align-self: flex-end;
+ margin-left: auto;
+ cursor: pointer;
+
+ padding: 3px 4px;
+ overflow: hidden;
+ cursor: pointer;
+ border-radius: 4px;
+
+ ${({ isFilterActive }) =>
+ isFilterActive &&
+ css`
+ background: linear-gradient(
+ var(--ag-icon-button-active-background-color),
+ var(--ag-icon-button-active-background-color)
+ );
+ ::after {
+ background-color: var(--ag-accent-color);
+ border-radius: 50%;
+ content: '';
+ height: 6px;
+ position: absolute;
+ right: 4px;
+ width: 6px;
+ }
+ `}
+
+ svg {
+ ${({ isFilterActive }) =>
+ isFilterActive &&
+ css`
+ clip-path: path('M8,0C8,4.415 11.585,8 16,8L16,16L0,16L0,0L8,0Z');
+ color: var(--ag-icon-button-active-color);
+ `}
+
+ :hover {
+ ${({ isFilterActive }) =>
+ !isFilterActive &&
+ css`
+ background-color: var(--ag-icon-button-hover-background-color);
+ box-shadow: 0 0 0 var(--ag-icon-button-background-spread)
+ var(--ag-icon-button-hover-background-color);
+ color: var(--ag-icon-button-hover-color);
+ border-radius: var(--ag-icon-button-border-radius);
+ `}
+ }
+ }
+`;
+
+export const MenuContainer = styled.div`
+ ${({ theme }) => `
+ min-width: ${theme.sizeUnit * 45}px;
+ padding: ${theme.sizeUnit}px 0;
+
+ .menu-item {
+ padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: ${theme.sizeUnit * 2}px;
+
+ &:hover {
+ background-color: ${theme.colors.primary.light4};
+ }
+ }
+
+ .menu-divider {
+ height: 1px;
+ background-color: ${theme.colors.grayscale.light2};
+ margin: ${theme.sizeUnit}px 0;
+ }
+ `}
+`;
+
+export const PopoverWrapper = styled.div`
+ position: relative;
+ display: inline-block;
+`;
+
+export const PopoverContainer = styled.div`
+ ${({ theme }) =>
+ `
+ position: fixed;
+ box-shadow: var(--ag-menu-shadow);
+ border-radius: ${theme.sizeUnit}px;
+ z-index: 99;
+ min-width: ${theme.sizeUnit * 50}px;
+ background: var(--ag-menu-background-color);
+ border: var(--ag-menu-border);
+ box-shadow: var(--ag-menu-shadow);
+ color: var(--ag-menu-text-color);
+
+ `}
+`;
+
+export const PaginationContainer = styled.div`
+ ${({ theme }) => `
+ border: 1px solid ${theme.colors.grayscale.light2};
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px;
+ border-top: 1px solid ${theme.colors.grayscale.light2};
+ font-size: ${theme.fontSize}px;
+ color: ${theme.colorTextBase};
+ transform: translateY(-${theme.sizeUnit}px);
+ background: ${theme.colorBgBase};
+ `}
+`;
+
+export const SelectWrapper = styled.div`
+ ${({ theme }) => `
+ position: relative;
+ margin-left: ${theme.sizeUnit * 2}px;
+ display: inline-block;
+ min-width: ${theme.sizeUnit * 17}px;
+ overflow: hidden;
+ `}
+`;
+
+export const PageInfo = styled.span`
+ ${({ theme }) => `
+ margin: 0 ${theme.sizeUnit * 6}px;
+ span {
+ font-weight: ${theme.fontWeightStrong};
+ }
+ `}
+`;
+
+export const PageCount = styled.span`
+ ${({ theme }) => `
+ span {
+ font-weight: ${theme.fontWeightStrong};
+ }
+ `}
+`;
+
+export const ButtonGroup = styled.div`
+ ${({ theme }) => `
+ display: flex;
+ gap: ${theme.sizeUnit * 3}px;
+ `}
+`;
+
+export const PageButton = styled.div<{ disabled?: boolean }>`
+ ${({ theme, disabled }) => `
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ height: ${theme.sizeUnit * 3}px;
+ width: ${theme.sizeUnit * 3}px;
+ fill: ${disabled ? theme.colors.grayscale.light1 : theme.colors.grayscale.dark2};
+ }
+ `}
+`;
+
+export const StyledSelect = styled(Select)`
+ ${({ theme }) => `
+ width: ${theme.sizeUnit * 30}px;
+ margin-right: ${theme.sizeUnit * 2}px;
+ `}
+`;
+
+// Time Comparison Visibility Styles
+export const InfoText = styled.div`
+ max-width: 242px;
+ ${({ theme }) => `
+ padding: 0 ${theme.sizeUnit * 2}px;
+ color: ${theme.colors.grayscale.base};
+ font-size: ${theme.fontSizeSM}px;
+ `}
+`;
+
+export const ColumnLabel = styled.span`
+ ${({ theme }) => `
+ color: ${theme.colors.grayscale.dark2};
+ `}
+`;
+
+export const CheckIconWrapper = styled.span`
+ ${({ theme }) => `
+ float: right;
+ font-size: ${theme.fontSizeSM}px;
+ `}
+`;
+
+// Text Cell Renderer Styles
+export const SummaryContainer = styled.div`
+ ${({ theme }) => `
+ display: flex;
+ align-items: center;
+ gap: ${theme.sizeUnit}px;
+ `}
+`;
+
+export const SummaryText = styled.div`
+ ${({ theme }) => `
+ font-weight: ${theme.fontWeightStrong};
+ `}
+`;
+
+// Table Container Styles
+export const StyledChartContainer = styled.div<{
+ height: number;
+}>`
+ ${({ theme, height }) => css`
+ height: ${height}px;
+
+ --ag-background-color: ${theme.colorBgBase};
+ --ag-foreground-color: ${theme.colorText};
+ --ag-header-background-color: ${theme.colorBgBase};
+ --ag-header-foreground-color: ${theme.colorText};
+
+ .dt-is-filter {
+ cursor: pointer;
+ :hover {
+ background-color: ${theme.colorPrimaryBgHover};
+ }
+ }
+
+ .dt-is-active-filter {
+ background: ${theme.colors.primary.light3};
+ :hover {
+ background-color: ${theme.colorPrimaryBgHover};
+ }
+ }
+
+ .dt-truncate-cell {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .dt-truncate-cell:hover {
+ overflow: visible;
+ white-space: normal;
+ height: auto;
+ }
+
+ .ag-container {
+ border-radius: 0px;
+ border: var(--ag-wrapper-border);
+ }
+
+ .ag-input-wrapper {
+ ::before {
+ z-index: 100;
+ }
+ }
+
+ .filter-popover {
+ z-index: 1 !important;
+ }
+
+ .search-container {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: ${theme.sizeUnit * 4}px;
+ }
+
+ .dropdown-controls-container {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ .time-comparison-dropdown {
+ display: flex;
+ padding-right: ${theme.sizeUnit * 4}px;
+ padding-top: ${theme.sizeUnit * 1.75}px;
+ height: fit-content;
+ }
+
+ .ag-header,
+ .ag-row,
+ .ag-spanned-row {
+ font-size: ${theme.fontSizeSM}px;
+ font-weight: ${theme.fontWeightStrong};
+ }
+
+ .ag-root-wrapper {
+ border-radius: 0px;
+ }
+ .search-by-text-container {
+ display: flex;
+ align-items: center;
+ }
+
+ .search-by-text {
+ margin-right: ${theme.sizeUnit * 2}px;
+ }
+
+ .ant-popover-inner {
+ padding: 0px;
+ }
+
+ .input-container {
+ margin-left: auto;
+ }
+
+ .input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ overflow: visible;
+ }
+
+ .input-wrapper svg {
+ pointer-events: none;
+ transform: translate(${theme.sizeUnit * 7}px, ${theme.sizeUnit / 2}px);
+ color: ${theme.colors.grayscale.base};
+ }
+
+ .input-wrapper input {
+ color: ${theme.colorText};
+ font-size: ${theme.fontSizeSM}px;
+ padding: ${theme.sizeUnit * 1.5}px ${theme.sizeUnit * 3}px
+ ${theme.sizeUnit * 1.5}px ${theme.sizeUnit * 8}px;
+ line-height: 1.8;
+ border-radius: ${theme.borderRadius}px;
+ border: 1px solid ${theme.colors.grayscale.light2};
+ background-color: transparent;
+ outline: none;
+
+ &:focus {
+ border-color: ${theme.colors.primary.base};
+ }
+
+ &::placeholder {
+ color: ${theme.colors.grayscale.light1};
+ }
+ }
+ `}
+`;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
new file mode 100644
index 00000000000..a53e3eb2c69
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
@@ -0,0 +1,741 @@
+/**
+ * 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 memoizeOne from 'memoize-one';
+import {
+ ComparisonType,
+ Currency,
+ CurrencyFormatter,
+ DataRecord,
+ ensureIsArray,
+ extractTimegrain,
+ FeatureFlag,
+ GenericDataType,
+ getMetricLabel,
+ getNumberFormatter,
+ getTimeFormatter,
+ getTimeFormatterForGranularity,
+ isFeatureEnabled,
+ NumberFormats,
+ QueryMode,
+ SMART_DATE_ID,
+ t,
+ TimeFormats,
+ TimeFormatter,
+} from '@superset-ui/core';
+
+import { isEmpty, isEqual } from 'lodash';
+import {
+ ConditionalFormattingConfig,
+ getColorFormatters,
+} from '@superset-ui/chart-controls';
+import isEqualColumns from './utils/isEqualColumns';
+import DateWithFormatter from './utils/DateWithFormatter';
+import {
+ DataColumnMeta,
+ TableChartProps,
+ AgGridTableChartTransformedProps,
+ TableColumnConfig,
+ ColorSchemeEnum,
+ BasicColorFormatterType,
+} from './types';
+
+const { PERCENT_3_POINT } = NumberFormats;
+const { DATABASE_DATETIME } = TimeFormats;
+
+function isNumeric(key: string, data: DataRecord[] = []) {
+ return data.every(
+ x => x[key] === null || x[key] === undefined || typeof x[key] === 'number',
+ );
+}
+
+function isPositiveNumber(value: string | number | null | undefined) {
+ const num = Number(value);
+ return (
+ value !== null &&
+ value !== undefined &&
+ value !== '' &&
+ !Number.isNaN(num) &&
+ num > 0
+ );
+}
+
+const processComparisonTotals = (
+ comparisonSuffix: string,
+ totals?: DataRecord[],
+): DataRecord | undefined => {
+ if (!totals) {
+ return totals;
+ }
+ const transformedTotals: DataRecord = {};
+ totals.map((totalRecord: DataRecord) =>
+ Object.keys(totalRecord).forEach(key => {
+ if (totalRecord[key] !== undefined && !key.includes(comparisonSuffix)) {
+ transformedTotals[`Main ${key}`] =
+ parseInt(transformedTotals[`Main ${key}`]?.toString() || '0', 10) +
+ parseInt(totalRecord[key]?.toString() || '0', 10);
+ transformedTotals[`# ${key}`] =
+ parseInt(transformedTotals[`# ${key}`]?.toString() || '0', 10) +
+ parseInt(
+ totalRecord[`${key}__${comparisonSuffix}`]?.toString() || '0',
+ 10,
+ );
+ const { valueDifference, percentDifferenceNum } = calculateDifferences(
+ transformedTotals[`Main ${key}`] as number,
+ transformedTotals[`# ${key}`] as number,
+ );
+ transformedTotals[`△ ${key}`] = valueDifference;
+ transformedTotals[`% ${key}`] = percentDifferenceNum;
+ }
+ }),
+ );
+
+ return transformedTotals;
+};
+
+const getComparisonColConfig = (
+ label: string,
+ parentColKey: string,
+ columnConfig: Record,
+) => {
+ const comparisonKey = `${label} ${parentColKey}`;
+ const comparisonColConfig = columnConfig[comparisonKey] || {};
+ return comparisonColConfig;
+};
+
+const getComparisonColFormatter = (
+ label: string,
+ parentCol: DataColumnMeta,
+ columnConfig: Record,
+ savedFormat: string | undefined,
+ savedCurrency: Currency | undefined,
+) => {
+ const currentColConfig = getComparisonColConfig(
+ label,
+ parentCol.key,
+ columnConfig,
+ );
+ const hasCurrency = currentColConfig.currencyFormat?.symbol;
+ const currentColNumberFormat =
+ // fallback to parent's number format if not set
+ currentColConfig.d3NumberFormat || parentCol.config?.d3NumberFormat;
+ let { formatter } = parentCol;
+ if (label === '%') {
+ formatter = getNumberFormatter(currentColNumberFormat || PERCENT_3_POINT);
+ } else if (currentColNumberFormat || hasCurrency) {
+ const currency = currentColConfig.currencyFormat || savedCurrency;
+ const numberFormat = currentColNumberFormat || savedFormat;
+ formatter = currency
+ ? new CurrencyFormatter({
+ d3Format: numberFormat,
+ currency,
+ })
+ : getNumberFormatter(numberFormat);
+ }
+ return formatter;
+};
+
+const calculateDifferences = (
+ originalValue: number,
+ comparisonValue: number,
+) => {
+ const valueDifference = originalValue - comparisonValue;
+ let percentDifferenceNum;
+ if (!originalValue && !comparisonValue) {
+ percentDifferenceNum = 0;
+ } else if (!originalValue || !comparisonValue) {
+ percentDifferenceNum = originalValue ? 1 : -1;
+ } else {
+ percentDifferenceNum =
+ (originalValue - comparisonValue) / Math.abs(comparisonValue);
+ }
+ return { valueDifference, percentDifferenceNum };
+};
+
+const processComparisonDataRecords = memoizeOne(
+ function processComparisonDataRecords(
+ originalData: DataRecord[] | undefined,
+ originalColumns: DataColumnMeta[],
+ comparisonSuffix: string,
+ ) {
+ // Transform data
+ return originalData?.map(originalItem => {
+ const transformedItem: DataRecord = {};
+ originalColumns.forEach(origCol => {
+ if (
+ (origCol.isMetric || origCol.isPercentMetric) &&
+ !origCol.key.includes(comparisonSuffix) &&
+ origCol.isNumeric
+ ) {
+ const originalValue = originalItem[origCol.key] || 0;
+ const comparisonValue = origCol.isMetric
+ ? originalItem?.[`${origCol.key}__${comparisonSuffix}`] || 0
+ : originalItem[`%${origCol.key.slice(1)}__${comparisonSuffix}`] ||
+ 0;
+ const { valueDifference, percentDifferenceNum } =
+ calculateDifferences(
+ originalValue as number,
+ comparisonValue as number,
+ );
+
+ transformedItem[`Main ${origCol.key}`] = originalValue;
+ transformedItem[`# ${origCol.key}`] = comparisonValue;
+ transformedItem[`△ ${origCol.key}`] = valueDifference;
+ transformedItem[`% ${origCol.key}`] = percentDifferenceNum;
+ }
+ });
+
+ Object.keys(originalItem).forEach(key => {
+ const isMetricOrPercentMetric = originalColumns.some(
+ col => col.key === key && (col.isMetric || col.isPercentMetric),
+ );
+ if (!isMetricOrPercentMetric) {
+ transformedItem[key] = originalItem[key];
+ }
+ });
+
+ return transformedItem;
+ });
+ },
+);
+
+const processComparisonColumns = (
+ columns: DataColumnMeta[],
+ props: TableChartProps,
+ comparisonSuffix: string,
+) =>
+ columns
+ .map(col => {
+ const {
+ datasource: { columnFormats, currencyFormats },
+ rawFormData: { column_config: columnConfig = {} },
+ } = props;
+ const savedFormat = columnFormats?.[col.key];
+ const savedCurrency = currencyFormats?.[col.key];
+ const originalLabel = col.label;
+ if (
+ (col.isMetric || col.isPercentMetric) &&
+ !col.key.includes(comparisonSuffix) &&
+ col.isNumeric
+ ) {
+ return [
+ {
+ ...col,
+ originalLabel,
+ metricName: col.key,
+ label: t('Main'),
+ key: `${t('Main')} ${col.key}`,
+ config: getComparisonColConfig(t('Main'), col.key, columnConfig),
+ formatter: getComparisonColFormatter(
+ t('Main'),
+ col,
+ columnConfig,
+ savedFormat,
+ savedCurrency,
+ ),
+ },
+ {
+ ...col,
+ originalLabel,
+ metricName: col.key,
+ label: `#`,
+ key: `# ${col.key}`,
+ config: getComparisonColConfig(`#`, col.key, columnConfig),
+ formatter: getComparisonColFormatter(
+ `#`,
+ col,
+ columnConfig,
+ savedFormat,
+ savedCurrency,
+ ),
+ },
+ {
+ ...col,
+ originalLabel,
+ metricName: col.key,
+ label: `△`,
+ key: `△ ${col.key}`,
+ config: getComparisonColConfig(`△`, col.key, columnConfig),
+ formatter: getComparisonColFormatter(
+ `△`,
+ col,
+ columnConfig,
+ savedFormat,
+ savedCurrency,
+ ),
+ },
+ {
+ ...col,
+ originalLabel,
+ metricName: col.key,
+ label: `%`,
+ key: `% ${col.key}`,
+ config: getComparisonColConfig(`%`, col.key, columnConfig),
+ formatter: getComparisonColFormatter(
+ `%`,
+ col,
+ columnConfig,
+ savedFormat,
+ savedCurrency,
+ ),
+ },
+ ];
+ }
+ if (
+ !col.isMetric &&
+ !col.isPercentMetric &&
+ !col.key.includes(comparisonSuffix)
+ ) {
+ return [col];
+ }
+ return [];
+ })
+ .flat();
+
+const serverPageLengthMap = new Map();
+
+const processDataRecords = memoizeOne(function processDataRecords(
+ data: DataRecord[] | undefined,
+ columns: DataColumnMeta[],
+) {
+ if (!data?.[0]) {
+ return data || [];
+ }
+ const timeColumns = columns.filter(
+ column => column.dataType === GenericDataType.Temporal,
+ );
+
+ if (timeColumns.length > 0) {
+ return data.map(x => {
+ const datum = { ...x };
+ timeColumns.forEach(({ key, formatter }) => {
+ // Convert datetime with a custom date class so we can use `String(...)`
+ // formatted value for global search, and `date.getTime()` for sorting.
+ datum[key] = new DateWithFormatter(x[key], {
+ formatter: formatter as TimeFormatter,
+ });
+ });
+ return datum;
+ });
+ }
+ return data;
+});
+
+const processColumns = memoizeOne(function processColumns(
+ props: TableChartProps,
+) {
+ const {
+ datasource: { columnFormats, currencyFormats, verboseMap },
+ rawFormData: {
+ table_timestamp_format: tableTimestampFormat,
+ metrics: metrics_,
+ percent_metrics: percentMetrics_,
+ column_config: columnConfig = {},
+ },
+ queriesData,
+ } = props;
+ const granularity = extractTimegrain(props.rawFormData);
+ const { data: records, colnames, coltypes } = queriesData[0] || {};
+ // convert `metrics` and `percentMetrics` to the key names in `data.records`
+ const metrics = (metrics_ ?? []).map(getMetricLabel);
+ const rawPercentMetrics = (percentMetrics_ ?? []).map(getMetricLabel);
+ // column names for percent metrics always starts with a '%' sign.
+ const percentMetrics = rawPercentMetrics.map((x: string) => `%${x}`);
+ const metricsSet = new Set(metrics);
+ const percentMetricsSet = new Set(percentMetrics);
+ const rawPercentMetricsSet = new Set(rawPercentMetrics);
+
+ const columns: DataColumnMeta[] = (colnames || [])
+ .filter(
+ key =>
+ // if a metric was only added to percent_metrics, they should not show up in the table.
+ !(rawPercentMetricsSet.has(key) && !metricsSet.has(key)),
+ )
+ .map((key: string, i) => {
+ const dataType = coltypes[i];
+ const config = columnConfig[key] || {};
+ // for the purpose of presentation, only numeric values are treated as metrics
+ // because users can also add things like `MAX(str_col)` as a metric.
+ const isMetric = metricsSet.has(key) && isNumeric(key, records);
+ const isPercentMetric = percentMetricsSet.has(key);
+ const label =
+ isPercentMetric && verboseMap?.hasOwnProperty(key.replace('%', ''))
+ ? `%${verboseMap[key.replace('%', '')]}`
+ : verboseMap?.[key] || key;
+ const isTime = dataType === GenericDataType.Temporal;
+ const isNumber = dataType === GenericDataType.Numeric;
+ const savedFormat = columnFormats?.[key];
+ const savedCurrency = currencyFormats?.[key];
+ const numberFormat = config.d3NumberFormat || savedFormat;
+ const currency = config.currencyFormat?.symbol
+ ? config.currencyFormat
+ : savedCurrency;
+
+ let formatter;
+
+ if (isTime || config.d3TimeFormat) {
+ // string types may also apply d3-time format
+ // pick adhoc format first, fallback to column level formats defined in
+ // datasource
+ const customFormat = config.d3TimeFormat || savedFormat;
+ const timeFormat = customFormat || tableTimestampFormat;
+ // When format is "Adaptive Formatting" (smart_date)
+ if (timeFormat === SMART_DATE_ID) {
+ if (granularity) {
+ // time column use formats based on granularity
+ formatter = getTimeFormatterForGranularity(granularity);
+ } else if (customFormat) {
+ // other columns respect the column-specific format
+ formatter = getTimeFormatter(customFormat);
+ } else if (isNumeric(key, records)) {
+ // if column is numeric values, it is considered a timestamp64
+ formatter = getTimeFormatter(DATABASE_DATETIME);
+ } else {
+ // if no column-specific format, print cell as is
+ formatter = String;
+ }
+ } else if (timeFormat) {
+ formatter = getTimeFormatter(timeFormat);
+ }
+ } else if (isPercentMetric) {
+ // percent metrics have a default format
+ formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
+ } else if (isMetric || (isNumber && (numberFormat || currency))) {
+ formatter = currency
+ ? new CurrencyFormatter({
+ d3Format: numberFormat,
+ currency,
+ })
+ : getNumberFormatter(numberFormat);
+ }
+ return {
+ key,
+ label,
+ dataType,
+ isNumeric: dataType === GenericDataType.Numeric,
+ isMetric,
+ isPercentMetric,
+ formatter,
+ config,
+ };
+ });
+ return [metrics, percentMetrics, columns] as [
+ typeof metrics,
+ typeof percentMetrics,
+ typeof columns,
+ ];
+}, isEqualColumns);
+
+/**
+ * Automatically set page size based on number of cells.
+ */
+const getPageSize = (
+ pageSize: number | string | null | undefined,
+ numRecords: number,
+ numColumns: number,
+) => {
+ if (typeof pageSize === 'number') {
+ // NaN is also has typeof === 'number'
+ return pageSize || 0;
+ }
+ if (typeof pageSize === 'string') {
+ return Number(pageSize) || 0;
+ }
+ // when pageSize not set, automatically add pagination if too many records
+ return numRecords * numColumns > 5000 ? 200 : 0;
+};
+
+const transformProps = (
+ chartProps: TableChartProps,
+): AgGridTableChartTransformedProps => {
+ const {
+ height,
+ width,
+ rawFormData: formData,
+ queriesData = [],
+ ownState: serverPaginationData,
+ filterState,
+ hooks: { setDataMask = () => {} },
+ emitCrossFilters,
+ } = chartProps;
+
+ const {
+ include_search: includeSearch = false,
+ page_length: pageLength,
+ order_desc: sortDesc = false,
+ slice_id,
+ time_compare,
+ comparison_type,
+ server_pagination: serverPagination = false,
+ server_page_length: serverPageLength = 10,
+ query_mode: queryMode,
+ align_pn: alignPositiveNegative = true,
+ show_cell_bars: showCellBars = true,
+ color_pn: colorPositiveNegative = true,
+ show_totals: showTotals,
+ conditional_formatting: conditionalFormatting,
+ comparison_color_enabled: comparisonColorEnabled = false,
+ comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
+ } = formData;
+
+ const allowRearrangeColumns = true;
+
+ const calculateBasicStyle = (
+ percentDifferenceNum: number,
+ colorOption: ColorSchemeEnum,
+ ) => {
+ if (percentDifferenceNum === 0) {
+ return {
+ arrow: '',
+ arrowColor: '',
+ // eslint-disable-next-line theme-colors/no-literal-colors
+ backgroundColor: 'rgba(0,0,0,0.2)',
+ };
+ }
+ const isPositive = percentDifferenceNum > 0;
+ const arrow = isPositive ? '↑' : '↓';
+ const arrowColor =
+ colorOption === ColorSchemeEnum.Green
+ ? isPositive
+ ? ColorSchemeEnum.Green
+ : ColorSchemeEnum.Red
+ : isPositive
+ ? ColorSchemeEnum.Red
+ : ColorSchemeEnum.Green;
+ const backgroundColor =
+ colorOption === ColorSchemeEnum.Green
+ ? `rgba(${isPositive ? '0,150,0' : '150,0,0'},0.2)`
+ : `rgba(${isPositive ? '150,0,0' : '0,150,0'},0.2)`;
+
+ return { arrow, arrowColor, backgroundColor };
+ };
+
+ const getBasicColorFormatter = memoizeOne(function getBasicColorFormatter(
+ originalData: DataRecord[] | undefined,
+ originalColumns: DataColumnMeta[],
+ selectedColumns?: ConditionalFormattingConfig[],
+ ) {
+ // Transform data
+ const relevantColumns = selectedColumns
+ ? originalColumns.filter(col =>
+ selectedColumns.some(scol => scol?.column?.includes(col.key)),
+ )
+ : originalColumns;
+
+ return originalData?.map(originalItem => {
+ const item: { [key: string]: BasicColorFormatterType } = {};
+ relevantColumns.forEach(origCol => {
+ if (
+ (origCol.isMetric || origCol.isPercentMetric) &&
+ !origCol.key.includes(ensureIsArray(timeOffsets)[0]) &&
+ origCol.isNumeric
+ ) {
+ const originalValue = originalItem[origCol.key] || 0;
+ const comparisonValue = origCol.isMetric
+ ? originalItem?.[
+ `${origCol.key}__${ensureIsArray(timeOffsets)[0]}`
+ ] || 0
+ : originalItem[
+ `%${origCol.key.slice(1)}__${ensureIsArray(timeOffsets)[0]}`
+ ] || 0;
+ const { percentDifferenceNum } = calculateDifferences(
+ originalValue as number,
+ comparisonValue as number,
+ );
+
+ if (selectedColumns) {
+ selectedColumns.forEach(col => {
+ if (col?.column?.includes(origCol.key)) {
+ const { arrow, arrowColor, backgroundColor } =
+ calculateBasicStyle(
+ percentDifferenceNum,
+ col.colorScheme || comparisonColorScheme,
+ );
+ item[col.column] = {
+ mainArrow: arrow,
+ arrowColor,
+ backgroundColor,
+ };
+ }
+ });
+ } else {
+ const { arrow, arrowColor, backgroundColor } = calculateBasicStyle(
+ percentDifferenceNum,
+ comparisonColorScheme,
+ );
+ item[`${origCol.key}`] = {
+ mainArrow: arrow,
+ arrowColor,
+ backgroundColor,
+ };
+ }
+ }
+ });
+ return item;
+ });
+ });
+
+ const getBasicColorFormatterForColumn = (
+ originalData: DataRecord[] | undefined,
+ originalColumns: DataColumnMeta[],
+ conditionalFormatting?: ConditionalFormattingConfig[],
+ ) => {
+ const selectedColumns = conditionalFormatting?.filter(
+ (config: ConditionalFormattingConfig) =>
+ config.column &&
+ (config.colorScheme === ColorSchemeEnum.Green ||
+ config.colorScheme === ColorSchemeEnum.Red),
+ );
+
+ return selectedColumns?.length
+ ? getBasicColorFormatter(originalData, originalColumns, selectedColumns)
+ : undefined;
+ };
+
+ const isUsingTimeComparison =
+ !isEmpty(time_compare) &&
+ queryMode === QueryMode.Aggregate &&
+ comparison_type === ComparisonType.Values &&
+ isFeatureEnabled(FeatureFlag.TableV2TimeComparisonEnabled);
+
+ let hasServerPageLengthChanged = false;
+
+ const pageLengthFromMap = serverPageLengthMap.get(slice_id);
+ if (!isEqual(pageLengthFromMap, serverPageLength)) {
+ serverPageLengthMap.set(slice_id, serverPageLength);
+ hasServerPageLengthChanged = true;
+ }
+
+ const [, percentMetrics, columns] = processColumns(chartProps);
+
+ const timeGrain = extractTimegrain(formData);
+
+ 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[] = [];
+
+ if (isUsingTimeComparison && !isEmpty(nonCustomNorInheritShifts)) {
+ timeOffsets = nonCustomNorInheritShifts;
+ }
+
+ // Shifts for custom or inherit time comparison
+ if (isUsingTimeComparison && !isEmpty(customOrInheritShifts)) {
+ if (customOrInheritShifts.includes('custom')) {
+ timeOffsets = timeOffsets.concat([formData.start_date_offset]);
+ }
+ if (customOrInheritShifts.includes('inherit')) {
+ timeOffsets = timeOffsets.concat(['inherit']);
+ }
+ }
+ const comparisonSuffix = isUsingTimeComparison
+ ? ensureIsArray(timeOffsets)[0]
+ : '';
+
+ let comparisonColumns: DataColumnMeta[] = [];
+
+ if (isUsingTimeComparison) {
+ comparisonColumns = processComparisonColumns(
+ columns,
+ chartProps,
+ comparisonSuffix,
+ );
+ }
+
+ let baseQuery;
+ let countQuery;
+ let rowCount;
+ let totalQuery;
+ if (serverPagination) {
+ [baseQuery, countQuery, totalQuery] = queriesData;
+ rowCount = (countQuery?.data?.[0]?.rowcount as number) ?? 0;
+ } else {
+ [baseQuery, totalQuery] = queriesData;
+ rowCount = baseQuery?.rowcount ?? 0;
+ }
+
+ const data = processDataRecords(baseQuery?.data, columns);
+ const comparisonData = processComparisonDataRecords(
+ baseQuery?.data,
+ columns,
+ comparisonSuffix,
+ );
+
+ const passedData = isUsingTimeComparison ? comparisonData || [] : data;
+ const passedColumns = isUsingTimeComparison ? comparisonColumns : columns;
+
+ const basicColorFormatters =
+ comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
+ const columnColorFormatters =
+ getColorFormatters(conditionalFormatting, passedData) ?? [];
+
+ const basicColorColumnFormatters = getBasicColorFormatterForColumn(
+ baseQuery?.data,
+ columns,
+ conditionalFormatting,
+ );
+
+ const hasPageLength = isPositiveNumber(pageLength);
+
+ const totals =
+ showTotals && queryMode === QueryMode.Aggregate
+ ? isUsingTimeComparison
+ ? processComparisonTotals(comparisonSuffix, totalQuery?.data)
+ : totalQuery?.data[0]
+ : undefined;
+
+ return {
+ height,
+ width,
+ data: passedData,
+ columns: passedColumns,
+ percentMetrics,
+ setDataMask,
+ sortDesc,
+ includeSearch,
+ pageSize: getPageSize(pageLength, data.length, columns.length),
+ filters: filterState.filters,
+ emitCrossFilters,
+ allowRearrangeColumns,
+ slice_id,
+ serverPagination,
+ rowCount,
+ serverPaginationData,
+ hasServerPageLengthChanged,
+ serverPageLength,
+ hasPageLength,
+ timeGrain,
+ isRawRecords: queryMode === QueryMode.Raw,
+ alignPositiveNegative,
+ showCellBars,
+ isUsingTimeComparison,
+ colorPositiveNegative,
+ totals,
+ showTotals,
+ columnColorFormatters,
+ basicColorColumnFormatters,
+ basicColorFormatters,
+ formData,
+ };
+};
+
+export default transformProps;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts
new file mode 100644
index 00000000000..249ee4150fe
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts
@@ -0,0 +1,263 @@
+/**
+ * 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 { ColorFormatters } from '@superset-ui/chart-controls';
+import {
+ NumberFormatter,
+ TimeFormatter,
+ TimeGranularity,
+ QueryFormMetric,
+ ChartProps,
+ DataRecord,
+ DataRecordValue,
+ DataRecordFilters,
+ GenericDataType,
+ QueryMode,
+ ChartDataResponseResult,
+ QueryFormData,
+ SetDataMaskHook,
+ CurrencyFormatter,
+ Currency,
+ JsonObject,
+ Metric,
+} from '@superset-ui/core';
+import { ColDef, Column, IHeaderParams } from 'ag-grid-community';
+import { CustomCellRendererProps } from 'ag-grid-react';
+
+export type CustomFormatter = (value: DataRecordValue) => string;
+
+export type TableColumnConfig = {
+ d3NumberFormat?: string;
+ d3SmallNumberFormat?: string;
+ d3TimeFormat?: string;
+ columnWidth?: number;
+ horizontalAlign?: 'left' | 'right' | 'center';
+ showCellBars?: boolean;
+ alignPositiveNegative?: boolean;
+ colorPositiveNegative?: boolean;
+ truncateLongCells?: boolean;
+ currencyFormat?: Currency;
+ visible?: boolean;
+ customColumnName?: string;
+ displayTypeIcon?: boolean;
+};
+
+export interface DataColumnMeta {
+ // `key` is what is called `label` in the input props
+ key: string;
+ // `label` is verbose column name used for rendering
+ label: string;
+ // `originalLabel` preserves the original label when time comparison transforms the labels
+ originalLabel?: string;
+ dataType: GenericDataType;
+ formatter?:
+ | TimeFormatter
+ | NumberFormatter
+ | CustomFormatter
+ | CurrencyFormatter;
+ isMetric?: boolean;
+ isPercentMetric?: boolean;
+ isNumeric?: boolean;
+ config?: TableColumnConfig;
+ isChildColumn?: boolean;
+}
+
+export interface TableChartData {
+ records: DataRecord[];
+ columns: string[];
+}
+
+export type TableChartFormData = QueryFormData & {
+ align_pn?: boolean;
+ color_pn?: boolean;
+ include_time?: boolean;
+ include_search?: boolean;
+ query_mode?: QueryMode;
+ page_length?: string | number | null; // null means auto-paginate
+ metrics?: QueryFormMetric[] | null;
+ percent_metrics?: QueryFormMetric[] | null;
+ timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null;
+ groupby?: QueryFormMetric[] | null;
+ all_columns?: QueryFormMetric[] | null;
+ order_desc?: boolean;
+ show_cell_bars?: boolean;
+ table_timestamp_format?: string;
+ time_grain_sqla?: TimeGranularity;
+ column_config?: Record;
+ allow_rearrange_columns?: boolean;
+};
+
+export interface TableChartProps extends ChartProps {
+ ownCurrentState?: {
+ pageSize?: number;
+ currentPage?: number;
+ };
+ rawFormData: TableChartFormData;
+ queriesData: ChartDataResponseResult[];
+}
+
+export type BasicColorFormatterType = {
+ backgroundColor: string;
+ arrowColor: string;
+ mainArrow: string;
+};
+
+export type SortByItem = {
+ id: string;
+ key: string;
+ desc?: boolean;
+};
+
+export type SearchOption = {
+ value: string;
+ label: string;
+};
+
+export interface ServerPaginationData {
+ pageSize?: number;
+ currentPage?: number;
+ sortBy?: SortByItem[];
+ searchText?: string;
+ searchColumn?: string;
+}
+
+export interface AgGridTableChartTransformedProps<
+ D extends DataRecord = DataRecord,
+> {
+ height: number;
+ width: number;
+
+ setDataMask: SetDataMaskHook;
+ data: D[];
+ columns: DataColumnMeta[];
+ pageSize?: number;
+ sortDesc?: boolean;
+ includeSearch?: boolean;
+ filters?: DataRecordFilters;
+ emitCrossFilters?: boolean;
+ allowRearrangeColumns?: boolean;
+ allowRenderHtml?: boolean;
+ slice_id: number;
+ serverPagination: boolean;
+ rowCount: number;
+ serverPaginationData: JsonObject;
+ percentMetrics: string[];
+ hasServerPageLengthChanged: boolean;
+ serverPageLength: number;
+ hasPageLength: boolean;
+ timeGrain: TimeGranularity | undefined;
+ isRawRecords: boolean;
+ alignPositiveNegative: boolean;
+ showCellBars: boolean;
+ isUsingTimeComparison: boolean;
+ colorPositiveNegative: boolean;
+ totals: DataRecord | undefined;
+ showTotals: boolean;
+ columnColorFormatters: ColorFormatters;
+ basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
+ basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
+ formData: TableChartFormData;
+}
+
+export enum ColorSchemeEnum {
+ 'Green' = 'Green',
+ 'Red' = 'Red',
+}
+
+export interface SortState {
+ colId: string;
+ sort: 'asc' | 'desc' | null;
+}
+
+export interface CustomContext {
+ initialSortState: SortState[];
+ onColumnHeaderClicked: (args: { column: SortState }) => void;
+}
+
+export interface CustomHeaderParams extends IHeaderParams {
+ context: CustomContext;
+ column: Column;
+ slice_id: number;
+}
+
+export interface UserProvidedColDef extends ColDef {
+ isMain?: boolean;
+ timeComparisonKey?: string;
+}
+
+export interface CustomColDef extends ColDef {
+ context?: {
+ isMetric?: boolean;
+ isPercentMetric?: boolean;
+ isNumeric?: boolean;
+ };
+}
+
+export type TableDataColumnMeta = DataColumnMeta & {
+ config?: TableColumnConfig;
+};
+
+export interface InputColumn {
+ key: string;
+ label: string;
+ dataType: number;
+ isNumeric: boolean;
+ isMetric: boolean;
+ isPercentMetric: boolean;
+ config: Record;
+ formatter?: Function;
+ originalLabel?: string;
+ metricName?: string;
+}
+
+export type CellRendererProps = CustomCellRendererProps & {
+ hasBasicColorFormatters: boolean | undefined;
+ col: InputColumn;
+ basicColorFormatters: {
+ [Key: string]: BasicColorFormatterType;
+ }[];
+ valueRange: any;
+ alignPositiveNegative: boolean;
+ colorPositiveNegative: boolean;
+ allowRenderHtml: boolean;
+ columns: InputColumn[];
+};
+
+export type Dataset = {
+ changed_by?: {
+ first_name: string;
+ last_name: string;
+ };
+ created_by?: {
+ first_name: string;
+ last_name: string;
+ };
+ changed_on_humanized: string;
+ created_on_humanized: string;
+ description: string;
+ table_name: string;
+ owners: {
+ first_name: string;
+ last_name: string;
+ }[];
+ columns?: Column[];
+ metrics?: Metric[];
+ verbose_map?: Record;
+};
+
+export default {};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/DateWithFormatter.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/DateWithFormatter.ts
new file mode 100644
index 00000000000..c92c2ca1abb
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/DateWithFormatter.ts
@@ -0,0 +1,55 @@
+/**
+ * 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 {
+ DataRecordValue,
+ normalizeTimestamp,
+ TimeFormatFunction,
+} from '@superset-ui/core';
+
+/**
+ * Extended Date object with a custom formatter, and retains the original input
+ * when the formatter is simple `String(..)`.
+ */
+export default class DateWithFormatter extends Date {
+ formatter: TimeFormatFunction;
+
+ input: DataRecordValue;
+
+ constructor(
+ input: DataRecordValue,
+ { formatter = String }: { formatter?: TimeFormatFunction } = {},
+ ) {
+ let value = input;
+ // assuming timestamps without a timezone is in UTC time
+ if (typeof value === 'string') {
+ value = normalizeTimestamp(value);
+ }
+
+ super(value as string);
+
+ this.input = input;
+ this.formatter = formatter;
+ this.toString = (): string => {
+ if (this.formatter === String) {
+ return String(this.input);
+ }
+ return this.formatter ? this.formatter(this) : Date.toString.call(this);
+ };
+ }
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/dateFilterComparator.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/dateFilterComparator.ts
new file mode 100644
index 00000000000..46fe46de5e8
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/dateFilterComparator.ts
@@ -0,0 +1,43 @@
+/**
+ * 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.
+ */
+
+const dateFilterComparator = (filterDate: Date, cellValue: Date) => {
+ const cellDate = new Date(cellValue);
+ cellDate.setHours(0, 0, 0, 0);
+ if (Number.isNaN(cellDate?.getTime())) return -1;
+
+ const cellDay = cellDate.getDate();
+ const cellMonth = cellDate.getMonth();
+ const cellYear = cellDate.getFullYear();
+
+ const filterDay = filterDate.getDate();
+ const filterMonth = filterDate.getMonth();
+ const filterYear = filterDate.getFullYear();
+
+ if (cellYear < filterYear) return -1;
+ if (cellYear > filterYear) return 1;
+ if (cellMonth < filterMonth) return -1;
+ if (cellMonth > filterMonth) return 1;
+ if (cellDay < filterDay) return -1;
+ if (cellDay > filterDay) return 1;
+
+ return 0;
+};
+
+export default dateFilterComparator;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/extent.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/extent.ts
new file mode 100644
index 00000000000..4b326800d2f
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/extent.ts
@@ -0,0 +1,48 @@
+/**
+ * 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 { isNil } from 'lodash';
+
+export default function extent(
+ values: T[],
+) {
+ let min: T | undefined;
+ let max: T | undefined;
+ // eslint-disable-next-line no-restricted-syntax
+ for (const value of values) {
+ if (value !== null) {
+ if (isNil(min)) {
+ if (value !== undefined) {
+ min = value;
+ max = value;
+ }
+ } else if (value !== undefined) {
+ if (min > value) {
+ min = value;
+ }
+ if (!isNil(max)) {
+ if (max < value) {
+ max = value;
+ }
+ }
+ }
+ }
+ }
+ return [min, max];
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/externalAPIs.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/externalAPIs.ts
new file mode 100644
index 00000000000..78b50f007c6
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/externalAPIs.ts
@@ -0,0 +1,38 @@
+/* eslint-disable camelcase */
+/**
+ * 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 { SetDataMaskHook } from '@superset-ui/core';
+import { SortByItem } from '../types';
+
+interface TableOwnState {
+ currentPage?: number;
+ pageSize?: number;
+ sortColumn?: string;
+ sortOrder?: 'asc' | 'desc';
+ searchText?: string;
+ sortBy?: SortByItem[];
+}
+
+export const updateTableOwnState = (
+ setDataMask: SetDataMaskHook = () => {},
+ modifiedOwnState: TableOwnState,
+) =>
+ setDataMask({
+ ownState: modifiedOwnState,
+ });
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/filterValueGetter.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/filterValueGetter.ts
new file mode 100644
index 00000000000..a6e8a7c75fb
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/filterValueGetter.ts
@@ -0,0 +1,34 @@
+/* eslint-disable camelcase */
+/**
+ * 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 { ValueGetterParams } from 'ag-grid-community';
+
+const filterValueGetter = (params: ValueGetterParams) => {
+ const raw = params.data[params.colDef.field as string];
+ const formatter = params.colDef.valueFormatter as Function;
+ if (!raw || !formatter) return null;
+ const formatted = formatter({
+ value: raw,
+ });
+
+ const numeric = parseFloat(String(formatted).replace('%', '').trim());
+ return Number.isNaN(numeric) ? null : numeric;
+};
+
+export default filterValueGetter;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts
new file mode 100644
index 00000000000..4b85e3fa9f6
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts
@@ -0,0 +1,115 @@
+/**
+ * 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 {
+ CurrencyFormatter,
+ DataRecordValue,
+ GenericDataType,
+ getNumberFormatter,
+ isDefined,
+ isProbablyHTML,
+ sanitizeHtml,
+} from '@superset-ui/core';
+import { ValueFormatterParams, ValueGetterParams } from 'ag-grid-community';
+import { DataColumnMeta, InputColumn } from '../types';
+import DateWithFormatter from './DateWithFormatter';
+
+/**
+ * Format text for cell value.
+ */
+function formatValue(
+ formatter: DataColumnMeta['formatter'],
+ value: DataRecordValue,
+): [boolean, string] {
+ // render undefined as empty string
+ if (value === undefined) {
+ return [false, ''];
+ }
+ // render null as `N/A`
+ if (
+ value === null ||
+ // null values in temporal columns are wrapped in a Date object, so make sure we
+ // handle them here too
+ (value instanceof DateWithFormatter && value.input === null)
+ ) {
+ return [false, 'N/A'];
+ }
+ if (formatter) {
+ return [false, formatter(value as number)];
+ }
+ if (typeof value === 'string') {
+ return isProbablyHTML(value) ? [true, sanitizeHtml(value)] : [false, value];
+ }
+ return [false, value.toString()];
+}
+
+export function formatColumnValue(
+ column: DataColumnMeta,
+ value: DataRecordValue,
+) {
+ const { dataType, formatter, config = {} } = column;
+ const isNumber = dataType === GenericDataType.Numeric;
+ const smallNumberFormatter =
+ config.d3SmallNumberFormat === undefined
+ ? formatter
+ : config.currencyFormat
+ ? new CurrencyFormatter({
+ d3Format: config.d3SmallNumberFormat,
+ currency: config.currencyFormat,
+ })
+ : getNumberFormatter(config.d3SmallNumberFormat);
+ return formatValue(
+ isNumber && typeof value === 'number' && Math.abs(value) < 1
+ ? smallNumberFormatter
+ : formatter,
+ value,
+ );
+}
+
+export const valueFormatter = (
+ params: ValueFormatterParams,
+ col: InputColumn,
+): string => {
+ const { value, node } = params;
+ if (
+ isDefined(value) &&
+ value !== '' &&
+ !(value instanceof DateWithFormatter && value.input === null)
+ ) {
+ return col.formatter?.(value) || value;
+ }
+ if (node?.level === -1) {
+ return '';
+ }
+ return 'N/A';
+};
+
+export const valueGetter = (params: ValueGetterParams, col: InputColumn) => {
+ // @ts-ignore
+ if (params?.colDef?.isMain) {
+ const modifiedColId = `Main ${params.column.getColId()}`;
+ return params.data[modifiedColId];
+ }
+ if (isDefined(params.data?.[params.column.getColId()])) {
+ return params.data[params.column.getColId()];
+ }
+ if (col.isNumeric) {
+ return undefined;
+ }
+ return '';
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getAggFunc.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getAggFunc.ts
new file mode 100644
index 00000000000..e84a34f3102
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getAggFunc.ts
@@ -0,0 +1,28 @@
+/* eslint-disable camelcase */
+/**
+ * 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 { CUSTOM_AGG_FUNCS } from '../consts';
+import { InputColumn } from '../types';
+
+export const getAggFunc = (col: InputColumn) =>
+ col.isMetric || col.isPercentMetric
+ ? CUSTOM_AGG_FUNCS.queryTotal
+ : col.isNumeric
+ ? 'sum'
+ : undefined;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellClass.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellClass.ts
new file mode 100644
index 00000000000..889d8c1225c
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellClass.ts
@@ -0,0 +1,46 @@
+/**
+ * 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 { CellClassParams } from 'ag-grid-community';
+import { InputColumn } from '../types';
+
+type GetCellClassParams = CellClassParams & {
+ col: InputColumn;
+ emitCrossFilters: boolean | undefined;
+};
+
+const getCellClass = (params: GetCellClassParams) => {
+ const { col, emitCrossFilters } = params;
+ const isActiveFilterValue = params?.context?.isActiveFilterValue;
+ let className = '';
+ if (emitCrossFilters) {
+ if (!col?.isMetric) {
+ className += ' dt-is-filter';
+ }
+ if (isActiveFilterValue?.(col?.key, params?.value)) {
+ className += ' dt-is-active-filter';
+ }
+ if (col?.config?.truncateLongCells) {
+ className += ' dt-truncate-cell';
+ }
+ }
+ return className;
+};
+
+export default getCellClass;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts
new file mode 100644
index 00000000000..035a3733f39
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCellStyle.ts
@@ -0,0 +1,82 @@
+/**
+ * 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 { ColorFormatters } from '@superset-ui/chart-controls';
+import { CellClassParams } from 'ag-grid-community';
+import { BasicColorFormatterType, InputColumn } from '../types';
+
+type CellStyleParams = CellClassParams & {
+ hasColumnColorFormatters: boolean | undefined;
+ columnColorFormatters: ColorFormatters;
+ hasBasicColorFormatters: boolean | undefined;
+ basicColorFormatters?: {
+ [Key: string]: BasicColorFormatterType;
+ }[];
+ col: InputColumn;
+};
+
+const getCellStyle = (params: CellStyleParams) => {
+ const {
+ value,
+ colDef,
+ rowIndex,
+ hasBasicColorFormatters,
+ basicColorFormatters,
+ hasColumnColorFormatters,
+ columnColorFormatters,
+ col,
+ node,
+ } = params;
+ let backgroundColor;
+ if (hasColumnColorFormatters) {
+ columnColorFormatters!
+ .filter(formatter => {
+ const colTitle = formatter?.column?.includes('Main')
+ ? formatter?.column?.replace('Main', '').trim()
+ : formatter?.column;
+ return colTitle === colDef.field;
+ })
+ .forEach(formatter => {
+ const formatterResult =
+ value || value === 0 ? formatter.getColorFromValue(value) : false;
+ if (formatterResult) {
+ backgroundColor = formatterResult;
+ }
+ });
+ }
+
+ if (
+ hasBasicColorFormatters &&
+ col?.metricName &&
+ node?.rowPinned !== 'bottom'
+ ) {
+ backgroundColor =
+ basicColorFormatters?.[rowIndex]?.[col.metricName]?.backgroundColor;
+ }
+
+ const textAlign =
+ col?.config?.horizontalAlign || (col?.isNumeric ? 'right' : 'left');
+
+ return {
+ backgroundColor: backgroundColor || '',
+ textAlign,
+ };
+};
+
+export default getCellStyle;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCrossFilterDataMask.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCrossFilterDataMask.ts
new file mode 100644
index 00000000000..a64817903bd
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getCrossFilterDataMask.ts
@@ -0,0 +1,103 @@
+/**
+ * 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 {
+ DataRecordFilters,
+ DataRecordValue,
+ DTTM_ALIAS,
+ ensureIsArray,
+ TimeGranularity,
+} from '@superset-ui/core';
+
+type GetCrossFilterDataMaskProps = {
+ key: string;
+ value: DataRecordValue;
+ filters?: DataRecordFilters;
+ timeGrain?: TimeGranularity;
+ isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
+ timestampFormatter: (value: DataRecordValue) => string;
+};
+
+export const getCrossFilterDataMask = ({
+ key,
+ value,
+ filters,
+ timeGrain,
+ isActiveFilterValue,
+ timestampFormatter,
+}: GetCrossFilterDataMaskProps) => {
+ let updatedFilters = { ...(filters || {}) };
+ if (filters && isActiveFilterValue(key, value)) {
+ updatedFilters = {};
+ } else {
+ updatedFilters = {
+ [key]: [value],
+ };
+ }
+ if (Array.isArray(updatedFilters[key]) && updatedFilters[key].length === 0) {
+ delete updatedFilters[key];
+ }
+
+ const groupBy = Object.keys(updatedFilters);
+ const groupByValues = Object.values(updatedFilters);
+ const labelElements: string[] = [];
+ groupBy.forEach(col => {
+ const isTimestamp = col === DTTM_ALIAS;
+ const filterValues = ensureIsArray(updatedFilters?.[col]);
+ if (filterValues.length) {
+ const valueLabels = filterValues.map(value =>
+ isTimestamp ? timestampFormatter(value) : value,
+ );
+ labelElements.push(`${valueLabels.join(', ')}`);
+ }
+ });
+
+ return {
+ dataMask: {
+ extraFormData: {
+ filters:
+ groupBy.length === 0
+ ? []
+ : groupBy.map(col => {
+ const val = ensureIsArray(updatedFilters?.[col]);
+ if (!val.length)
+ return {
+ col,
+ op: 'IS NULL' as const,
+ };
+ return {
+ col,
+ op: 'IN' as const,
+ val: val.map(el => (el instanceof Date ? el.getTime() : el!)),
+ grain: col === DTTM_ALIAS ? timeGrain : undefined,
+ };
+ }),
+ },
+ filterState: {
+ label: labelElements.join(', '),
+ value: groupByValues.length ? groupByValues : null,
+ filters:
+ updatedFilters && Object.keys(updatedFilters).length
+ ? updatedFilters
+ : null,
+ },
+ },
+ isCurrentValueSelected: isActiveFilterValue(key, value),
+ };
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getInitialSortState.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getInitialSortState.ts
new file mode 100644
index 00000000000..8e77d781dd6
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/getInitialSortState.ts
@@ -0,0 +1,65 @@
+/**
+ * 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.
+ */
+// All ag grid sort related stuff
+import { GridState, SortModelItem } from 'ag-grid-community';
+import { SortByItem } from '../types';
+
+const getInitialSortState = (sortBy?: SortByItem[]): SortModelItem[] => {
+ if (Array.isArray(sortBy) && sortBy.length > 0) {
+ return [
+ {
+ colId: sortBy[0]?.id,
+ sort: sortBy[0]?.desc ? 'desc' : 'asc',
+ },
+ ];
+ }
+ return [];
+};
+
+export const shouldSort = ({
+ colId,
+ sortDir,
+ percentMetrics,
+ serverPagination,
+ gridInitialState,
+}: {
+ colId: string;
+ sortDir: string;
+ percentMetrics: string[];
+ serverPagination: boolean;
+ gridInitialState: GridState;
+}) => {
+ // percent metrics are not sortable
+ if (percentMetrics.includes(colId)) return false;
+ // if server pagination is not enabled, return false
+ // since this is server pagination sort
+ if (!serverPagination) return false;
+
+ const {
+ colId: initialColId = '',
+ sort: initialSortDir,
+ }: Partial = gridInitialState?.sort?.sortModel?.[0] || {};
+
+ // if the initial sort is the same as the current sort, return false
+ if (initialColId === colId && initialSortDir === sortDir) return false;
+
+ return true;
+};
+
+export default getInitialSortState;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
new file mode 100644
index 00000000000..28731c73c27
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/isEqualColumns.ts
@@ -0,0 +1,46 @@
+/**
+ * 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 { isEqualArray } from '@superset-ui/core';
+import { TableChartProps } from '../types';
+
+export default function isEqualColumns(
+ propsA: TableChartProps[],
+ propsB: TableChartProps[],
+) {
+ const a = propsA[0];
+ const b = propsB[0];
+ return (
+ a.datasource.columnFormats === b.datasource.columnFormats &&
+ a.datasource.currencyFormats === b.datasource.currencyFormats &&
+ a.datasource.verboseMap === b.datasource.verboseMap &&
+ a.formData.tableTimestampFormat === b.formData.tableTimestampFormat &&
+ a.formData.timeGrainSqla === b.formData.timeGrainSqla &&
+ JSON.stringify(a.formData.columnConfig || null) ===
+ JSON.stringify(b.formData.columnConfig || null) &&
+ isEqualArray(a.formData.metrics, b.formData.metrics) &&
+ isEqualArray(a.queriesData?.[0]?.colnames, b.queriesData?.[0]?.colnames) &&
+ isEqualArray(a.queriesData?.[0]?.coltypes, b.queriesData?.[0]?.coltypes) &&
+ JSON.stringify(a.formData.extraFilters || null) ===
+ JSON.stringify(b.formData.extraFilters || null) &&
+ JSON.stringify(a.formData.extraFormData || null) ===
+ JSON.stringify(b.formData.extraFormData || null) &&
+ JSON.stringify(a.rawFormData.column_config || null) ===
+ JSON.stringify(b.rawFormData.column_config || null)
+ );
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts
new file mode 100644
index 00000000000..67671aafab4
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useColDefs.ts
@@ -0,0 +1,327 @@
+/* eslint-disable camelcase */
+/**
+ * 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 { ColDef } from 'ag-grid-community';
+import { useCallback, useMemo } from 'react';
+import { DataRecord, GenericDataType } from '@superset-ui/core';
+import { ColorFormatters } from '@superset-ui/chart-controls';
+import { extent as d3Extent, max as d3Max } from 'd3-array';
+import {
+ BasicColorFormatterType,
+ CellRendererProps,
+ InputColumn,
+} from '../types';
+import getCellClass from './getCellClass';
+import filterValueGetter from './filterValueGetter';
+import dateFilterComparator from './dateFilterComparator';
+import { getAggFunc } from './getAggFunc';
+import { TextCellRenderer } from '../renderers/TextCellRenderer';
+import { NumericCellRenderer } from '../renderers/NumericCellRenderer';
+import CustomHeader from '../AgGridTable/components/CustomHeader';
+import { valueFormatter, valueGetter } from './formatValue';
+import getCellStyle from './getCellStyle';
+
+interface InputData {
+ [key: string]: any;
+}
+
+type UseColDefsProps = {
+ columns: InputColumn[];
+ data: InputData[];
+ serverPagination: boolean;
+ isRawRecords: boolean;
+ defaultAlignPN: boolean;
+ showCellBars: boolean;
+ colorPositiveNegative: boolean;
+ totals: DataRecord | undefined;
+ columnColorFormatters: ColorFormatters;
+ allowRearrangeColumns?: boolean;
+ basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
+ isUsingTimeComparison?: boolean;
+ emitCrossFilters?: boolean;
+ alignPositiveNegative: boolean;
+ slice_id: number;
+};
+
+type ValueRange = [number, number];
+
+function getValueRange(
+ key: string,
+ alignPositiveNegative: boolean,
+ data: InputData[],
+) {
+ if (typeof data?.[0]?.[key] === 'number') {
+ const nums = data.map(row => row[key]) as number[];
+ return (
+ alignPositiveNegative ? [0, d3Max(nums.map(Math.abs))] : d3Extent(nums)
+ ) as ValueRange;
+ }
+ return null;
+}
+
+const getCellDataType = (col: InputColumn) => {
+ switch (col.dataType) {
+ case GenericDataType.Numeric:
+ return 'number';
+ case GenericDataType.Temporal:
+ return 'date';
+ case GenericDataType.Boolean:
+ return 'boolean';
+ default:
+ return 'text';
+ }
+};
+
+const getFilterType = (col: InputColumn) => {
+ switch (col.dataType) {
+ case GenericDataType.Numeric:
+ return 'agNumberColumnFilter';
+ case GenericDataType.String:
+ return 'agMultiColumnFilter';
+ case GenericDataType.Temporal:
+ return 'agDateColumnFilter';
+ default:
+ return true;
+ }
+};
+
+function getHeaderLabel(col: InputColumn) {
+ let headerLabel: string | undefined;
+
+ const hasOriginalLabel = !!col?.originalLabel;
+ const isMain = col?.key?.includes('Main');
+ const hasDisplayTypeIcon = col?.config?.displayTypeIcon !== false;
+ const hasCustomColumnName = !!col?.config?.customColumnName;
+
+ if (hasOriginalLabel && hasCustomColumnName) {
+ if ('displayTypeIcon' in col.config) {
+ headerLabel =
+ hasDisplayTypeIcon && !isMain
+ ? `${col.label} ${col.config.customColumnName}`
+ : col.config.customColumnName;
+ } else {
+ headerLabel = col.config.customColumnName;
+ }
+ } else if (hasOriginalLabel && isMain) {
+ headerLabel = col.originalLabel;
+ } else if (hasOriginalLabel && !hasDisplayTypeIcon) {
+ headerLabel = '';
+ } else {
+ headerLabel = col?.label;
+ }
+ return headerLabel || '';
+}
+
+export const useColDefs = ({
+ columns,
+ data,
+ serverPagination,
+ isRawRecords,
+ defaultAlignPN,
+ showCellBars,
+ colorPositiveNegative,
+ totals,
+ columnColorFormatters,
+ allowRearrangeColumns,
+ basicColorFormatters,
+ isUsingTimeComparison,
+ emitCrossFilters,
+ alignPositiveNegative,
+ slice_id,
+}: UseColDefsProps) => {
+ const getCommonColProps = useCallback(
+ (
+ col: InputColumn,
+ ): ColDef & {
+ isMain: boolean;
+ } => {
+ const {
+ config,
+ isMetric,
+ isPercentMetric,
+ isNumeric,
+ key: originalKey,
+ dataType,
+ originalLabel,
+ } = col;
+
+ const alignPN =
+ config.alignPositiveNegative === undefined
+ ? defaultAlignPN
+ : config.alignPositiveNegative;
+
+ const hasColumnColorFormatters =
+ isNumeric &&
+ Array.isArray(columnColorFormatters) &&
+ columnColorFormatters.length > 0;
+
+ const hasBasicColorFormatters =
+ isUsingTimeComparison &&
+ Array.isArray(basicColorFormatters) &&
+ basicColorFormatters.length > 0;
+
+ const isMain = originalKey?.includes('Main');
+ const colId = isMain
+ ? originalKey.replace('Main', '').trim()
+ : originalKey;
+ const isTextColumn =
+ dataType === GenericDataType.String ||
+ dataType === GenericDataType.Temporal;
+
+ const valueRange =
+ !hasBasicColorFormatters &&
+ !hasColumnColorFormatters &&
+ showCellBars &&
+ (config.showCellBars ?? true) &&
+ (isMetric || isRawRecords || isPercentMetric) &&
+ getValueRange(originalKey, alignPN || alignPositiveNegative, data);
+
+ const filter = getFilterType(col);
+
+ return {
+ field: colId,
+ headerName: getHeaderLabel(col),
+ valueFormatter: p => valueFormatter(p, col),
+ valueGetter: p => valueGetter(p, col),
+ cellStyle: p =>
+ getCellStyle({
+ ...p,
+ hasColumnColorFormatters,
+ columnColorFormatters,
+ hasBasicColorFormatters,
+ basicColorFormatters,
+ col,
+ }),
+ cellClass: p =>
+ getCellClass({
+ ...p,
+ col,
+ emitCrossFilters,
+ }),
+ minWidth: config?.columnWidth ?? 100,
+ filter,
+ ...(isPercentMetric && {
+ filterValueGetter,
+ }),
+ ...(dataType === GenericDataType.Temporal && {
+ filterParams: {
+ comparator: dateFilterComparator,
+ },
+ }),
+ cellDataType: getCellDataType(col),
+ defaultAggFunc: getAggFunc(col),
+ initialAggFunc: getAggFunc(col),
+ ...(!(isMetric || isPercentMetric) && {
+ allowedAggFuncs: [
+ 'sum',
+ 'min',
+ 'max',
+ 'count',
+ 'avg',
+ 'first',
+ 'last',
+ ],
+ }),
+ cellRenderer: (p: CellRendererProps) =>
+ isTextColumn ? TextCellRenderer(p) : NumericCellRenderer(p),
+ cellRendererParams: {
+ allowRenderHtml: true,
+ columns,
+ hasBasicColorFormatters,
+ col,
+ basicColorFormatters,
+ valueRange,
+ alignPositiveNegative: alignPN || alignPositiveNegative,
+ colorPositiveNegative,
+ },
+ context: {
+ isMetric,
+ isPercentMetric,
+ isNumeric,
+ },
+ lockPinned: !allowRearrangeColumns,
+ sortable: !serverPagination || !isPercentMetric,
+ ...(serverPagination && {
+ headerComponent: CustomHeader,
+ comparator: () => 0,
+ headerComponentParams: {
+ slice_id,
+ },
+ }),
+ isMain,
+ ...(!isMain &&
+ originalLabel && {
+ columnGroupShow: 'open',
+ }),
+ ...(originalLabel && {
+ timeComparisonKey: originalLabel,
+ }),
+ wrapText: !config?.truncateLongCells,
+ autoHeight: !config?.truncateLongCells,
+ };
+ },
+ [
+ columns,
+ data,
+ defaultAlignPN,
+ columnColorFormatters,
+ basicColorFormatters,
+ showCellBars,
+ colorPositiveNegative,
+ isUsingTimeComparison,
+ isRawRecords,
+ emitCrossFilters,
+ allowRearrangeColumns,
+ serverPagination,
+ alignPositiveNegative,
+ ],
+ );
+
+ const stringifiedCols = JSON.stringify(columns);
+
+ const colDefs = useMemo(() => {
+ const groupIndexMap = new Map();
+
+ return columns.reduce((acc, col) => {
+ const colDef = getCommonColProps(col);
+
+ if (col?.originalLabel) {
+ if (groupIndexMap.has(col.originalLabel)) {
+ const groupIdx = groupIndexMap.get(col.originalLabel)!;
+ (acc[groupIdx] as { children: ColDef[] }).children.push(colDef);
+ } else {
+ const group = {
+ headerName: col.originalLabel,
+ marryChildren: true,
+ openByDefault: true,
+ children: [colDef],
+ };
+ groupIndexMap.set(col.originalLabel, acc.length);
+ acc.push(group);
+ }
+ } else {
+ acc.push(colDef);
+ }
+
+ return acc;
+ }, []);
+ }, [stringifiedCols, getCommonColProps]);
+
+ return colDefs;
+};
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useTableTheme.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useTableTheme.ts
new file mode 100644
index 00000000000..7736fe584a1
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/useTableTheme.ts
@@ -0,0 +1,42 @@
+/**
+ * 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 { useTheme } from '@superset-ui/core';
+import {
+ colorSchemeDark,
+ colorSchemeLight,
+ themeQuartz,
+} from 'ag-grid-community';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import tinycolor from 'tinycolor2';
+
+export const useIsDark = () => {
+ const theme = useTheme();
+ return tinycolor(theme.colorBgContainer).isDark();
+};
+
+const useTableTheme = () => {
+ const baseTheme = themeQuartz;
+ const isDarkTheme = useIsDark();
+ const tableTheme = isDarkTheme
+ ? baseTheme.withPart(colorSchemeDark)
+ : baseTheme.withPart(colorSchemeLight);
+ return tableTheme;
+};
+
+export default useTableTheme;
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json b/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json
new file mode 100644
index 00000000000..f60297e2489
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "declarationDir": "lib",
+ "outDir": "lib",
+ "rootDir": "src"
+ },
+ "exclude": ["lib", "test"],
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*", "types/**/*", "../../types/**/*"],
+ "references": [
+ {
+ "path": "../../packages/superset-ui-chart-controls"
+ },
+ {
+ "path": "../../packages/superset-ui-core"
+ }
+ ]
+}
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/types/external.d.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/types/external.d.ts
new file mode 100644
index 00000000000..eb7f1895cb7
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/types/external.d.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+declare module '*.png';
+declare module '*.jpg';
+declare module 'regenerator-runtime/runtime';
diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js
index 387570a3262..4b6227fce6c 100644
--- a/superset-frontend/src/visualizations/presets/MainPreset.js
+++ b/superset-frontend/src/visualizations/presets/MainPreset.js
@@ -80,6 +80,7 @@ import {
import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table';
import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
import { FilterPlugins } from 'src/constants';
+import AgGridTableChartPlugin from '@superset-ui/plugin-chart-ag-grid-table';
import TimeTableChartPlugin from '../TimeTable';
export default class MainPreset extends Preset {
@@ -94,6 +95,10 @@ export default class MainPreset extends Preset {
]
: [];
+ const agGridTablePlugin = isFeatureEnabled(FeatureFlag.AgGridTableEnabled)
+ ? [new AgGridTableChartPlugin().configure({ key: VizType.TableAgGrid })]
+ : [];
+
super({
name: 'Legacy charts',
presets: [new DeckGLChartPreset()],
@@ -187,6 +192,7 @@ export default class MainPreset extends Preset {
],
}).configure({ key: VizType.Cartodiagram }),
...experimentalPlugins,
+ ...agGridTablePlugin,
],
});
}
diff --git a/superset/config.py b/superset/config.py
index 6d847efe72e..8463ead8ba9 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -579,6 +579,10 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# Allow metrics and columns to be grouped into (potentially nested) folders in the
# chart builder
"DATASET_FOLDERS": False,
+ # Enable Table V2 Viz plugin
+ "AG_GRID_TABLE_ENABLED": False,
+ # Enable Table v2 time comparison feature
+ "TABLE_V2_TIME_COMPARISON_ENABLED": False,
}
# ------------------------------