Skip to content

Commit 53503e3

Browse files
authored
fix(Table chart): fix percentage metric column (apache#34175)
1 parent 246181a commit 53503e3

File tree

6 files changed

+279
-21
lines changed

6 files changed

+279
-21
lines changed

superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ export * from './getStandardizedControls';
2929
export * from './getTemporalColumns';
3030
export * from './displayTimeRelatedControls';
3131
export * from './colorControls';
32+
export * from './metricColumnFilter';
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { QueryFormMetric, SqlaFormData } from '@superset-ui/core';
21+
import {
22+
shouldSkipMetricColumn,
23+
isRegularMetric,
24+
isPercentMetric,
25+
} from './metricColumnFilter';
26+
27+
const createMetric = (label: string): QueryFormMetric =>
28+
({
29+
label,
30+
expressionType: 'SIMPLE',
31+
column: { column_name: label },
32+
aggregate: 'SUM',
33+
}) as QueryFormMetric;
34+
35+
describe('metricColumnFilter', () => {
36+
const createFormData = (
37+
metrics: string[],
38+
percentMetrics: string[],
39+
): SqlaFormData =>
40+
({
41+
datasource: 'test_datasource',
42+
viz_type: 'table',
43+
metrics: metrics.map(createMetric),
44+
percent_metrics: percentMetrics.map(createMetric),
45+
}) as SqlaFormData;
46+
47+
describe('shouldSkipMetricColumn', () => {
48+
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
49+
const colnames = ['metric1', '%metric1'];
50+
const formData = createFormData([], ['metric1']);
51+
52+
const result = shouldSkipMetricColumn({
53+
colname: 'metric1',
54+
colnames,
55+
formData,
56+
});
57+
58+
expect(result).toBe(true);
59+
});
60+
61+
it('should not skip if column is also a regular metric', () => {
62+
const colnames = ['metric1', '%metric1'];
63+
const formData = createFormData(['metric1'], ['metric1']);
64+
65+
const result = shouldSkipMetricColumn({
66+
colname: 'metric1',
67+
colnames,
68+
formData,
69+
});
70+
71+
expect(result).toBe(false);
72+
});
73+
74+
it('should not skip if column starts with %', () => {
75+
const colnames = ['%metric1'];
76+
const formData = createFormData(['metric1'], []);
77+
78+
const result = shouldSkipMetricColumn({
79+
colname: '%metric1',
80+
colnames,
81+
formData,
82+
});
83+
84+
expect(result).toBe(false);
85+
});
86+
87+
it('should not skip if no prefixed version exists', () => {
88+
const colnames = ['metric1'];
89+
const formData = createFormData([], ['metric1']);
90+
91+
const result = shouldSkipMetricColumn({
92+
colname: 'metric1',
93+
colnames,
94+
formData,
95+
});
96+
97+
expect(result).toBe(false);
98+
});
99+
});
100+
101+
describe('isRegularMetric', () => {
102+
it('should return true for regular metrics', () => {
103+
const formData = createFormData(['metric1', 'metric2'], []);
104+
expect(isRegularMetric('metric1', formData)).toBe(true);
105+
expect(isRegularMetric('metric2', formData)).toBe(true);
106+
});
107+
108+
it('should return false for non-metrics', () => {
109+
const formData = createFormData(['metric1'], []);
110+
expect(isRegularMetric('non_metric', formData)).toBe(false);
111+
});
112+
113+
it('should return false for percentage metrics', () => {
114+
const formData = createFormData([], ['percent_metric1']);
115+
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
116+
});
117+
});
118+
119+
describe('isPercentMetric', () => {
120+
it('should return true for percentage metrics', () => {
121+
const formData = createFormData([], ['percent_metric1']);
122+
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
123+
});
124+
125+
it('should return false for non-percentage metrics', () => {
126+
const formData = createFormData(['regular_metric'], []);
127+
expect(isPercentMetric('regular_metric', formData)).toBe(false);
128+
});
129+
130+
it('should return false for regular metrics', () => {
131+
const formData = createFormData(['metric1'], []);
132+
expect(isPercentMetric('metric1', formData)).toBe(false);
133+
});
134+
});
135+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import {
21+
QueryFormMetric,
22+
getMetricLabel,
23+
SqlaFormData,
24+
} from '@superset-ui/core';
25+
26+
export interface MetricColumnFilterParams {
27+
colname: string;
28+
colnames: string[];
29+
formData: SqlaFormData;
30+
}
31+
32+
/**
33+
* Determines if a column should be skipped based on metric filtering logic.
34+
*
35+
* This function implements the logic to skip unprefixed percent metric columns
36+
* if a prefixed version exists, but doesn't skip if it's also a regular metric.
37+
*
38+
* @param params - The parameters for metric column filtering
39+
* @returns true if the column should be skipped, false otherwise
40+
*/
41+
export function shouldSkipMetricColumn({
42+
colname,
43+
colnames,
44+
formData,
45+
}: MetricColumnFilterParams): boolean {
46+
if (!colname) {
47+
return false;
48+
}
49+
50+
// Check if this column name exists as a percent metric in form data
51+
const isPercentMetric = formData.percent_metrics?.some(
52+
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
53+
);
54+
55+
// Check if this column name exists as a regular metric in form data
56+
const isRegularMetric = formData.metrics?.some(
57+
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
58+
);
59+
60+
// Check if there's a prefixed version of this column in the column list
61+
const hasPrefixedVersion = colnames.includes(`%${colname}`);
62+
63+
// Skip if: has prefixed version AND is percent metric AND is NOT regular metric
64+
return hasPrefixedVersion && isPercentMetric && !isRegularMetric;
65+
}
66+
67+
/**
68+
* Determines if a column is a regular metric.
69+
*
70+
* @param colname - The column name to check
71+
* @param formData - The form data containing metrics
72+
* @returns true if the column is a regular metric, false otherwise
73+
*/
74+
export function isRegularMetric(
75+
colname: string,
76+
formData: SqlaFormData,
77+
): boolean {
78+
return !!formData.metrics?.some(metric => getMetricLabel(metric) === colname);
79+
}
80+
81+
/**
82+
* Determines if a column is a percentage metric.
83+
*
84+
* @param colname: string,
85+
* @param formData - The form data containing percent_metrics
86+
* @returns true if the column is a percentage metric, false otherwise
87+
*/
88+
export function isPercentMetric(
89+
colname: string,
90+
formData: SqlaFormData,
91+
): boolean {
92+
return !!formData.percent_metrics?.some(
93+
(metric: QueryFormMetric) => `%${getMetricLabel(metric)}` === colname,
94+
);
95+
}

superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,19 @@ import {
3636
QueryModeLabel,
3737
sections,
3838
sharedControls,
39+
shouldSkipMetricColumn,
40+
isRegularMetric,
41+
isPercentMetric,
3942
} from '@superset-ui/chart-controls';
4043
import {
4144
ensureIsArray,
4245
FeatureFlag,
4346
GenericDataType,
44-
getMetricLabel,
4547
isAdhocColumn,
4648
isFeatureEnabled,
4749
isPhysicalColumn,
4850
legacyValidateInteger,
4951
QueryFormColumn,
50-
QueryFormMetric,
5152
QueryMode,
5253
SMART_DATE_ID,
5354
t,
@@ -533,14 +534,25 @@ const config: ControlPanelConfig = {
533534
)
534535
.forEach((colname, index) => {
535536
if (
536-
explore.form_data.metrics?.some(
537-
metric => getMetricLabel(metric) === colname,
538-
) ||
539-
explore.form_data.percent_metrics?.some(
540-
(metric: QueryFormMetric) =>
541-
getMetricLabel(metric) === colname,
542-
)
537+
shouldSkipMetricColumn({
538+
colname,
539+
colnames,
540+
formData: explore.form_data,
541+
})
543542
) {
543+
return;
544+
}
545+
546+
const isMetric = isRegularMetric(
547+
colname,
548+
explore.form_data,
549+
);
550+
const isPercentMetricValue = isPercentMetric(
551+
colname,
552+
explore.form_data,
553+
);
554+
555+
if (isMetric || isPercentMetricValue) {
544556
const comparisonColumns =
545557
generateComparisonColumns(colname);
546558
comparisonColumns.forEach((name, idx) => {

superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,17 @@ import {
3636
QueryModeLabel,
3737
sections,
3838
sharedControls,
39+
shouldSkipMetricColumn,
40+
isRegularMetric,
41+
isPercentMetric,
3942
} from '@superset-ui/chart-controls';
4043
import {
4144
ensureIsArray,
4245
GenericDataType,
43-
getMetricLabel,
4446
isAdhocColumn,
4547
isPhysicalColumn,
4648
legacyValidateInteger,
4749
QueryFormColumn,
48-
QueryFormMetric,
4950
QueryMode,
5051
SMART_DATE_ID,
5152
t,
@@ -589,15 +590,29 @@ const config: ControlPanelConfig = {
589590
last(colname.split('__')) !== timeComparisonValue,
590591
)
591592
.forEach((colname, index) => {
593+
// Skip unprefixed percent metric columns if a prefixed version exists
594+
// But don't skip if it's also a regular metric
592595
if (
593-
explore.form_data.metrics?.some(
594-
metric => getMetricLabel(metric) === colname,
595-
) ||
596-
explore.form_data.percent_metrics?.some(
597-
(metric: QueryFormMetric) =>
598-
getMetricLabel(metric) === colname,
599-
)
596+
shouldSkipMetricColumn({
597+
colname,
598+
colnames,
599+
formData: explore.form_data,
600+
})
600601
) {
602+
return;
603+
}
604+
605+
const isMetric = isRegularMetric(
606+
colname,
607+
explore.form_data,
608+
);
609+
const isPercentMetricValue = isPercentMetric(
610+
colname,
611+
explore.form_data,
612+
);
613+
614+
// Generate comparison columns for metrics (time comparison feature)
615+
if (isMetric || isPercentMetricValue) {
601616
const comparisonColumns =
602617
generateComparisonColumns(colname);
603618
comparisonColumns.forEach((name, idx) => {

superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ export default memo(function ColumnConfigItem({
101101
)}
102102
trigger="click"
103103
placement="right"
104-
styles={{ body: { width, height } }}
105-
rootClassName="column-config-popover"
104+
style={{ width, height }}
105+
className="column-config-popover"
106106
>
107107
<div css={outerContainerStyle}>
108108
<div css={nameContainerStyle}>
@@ -117,7 +117,7 @@ export default memo(function ColumnConfigItem({
117117
iconColor={colors.grayscale.base}
118118
/>
119119
)}
120-
<Icons.CaretRightOutlined css={caretIconStyle} />
120+
<Icons.RightOutlined css={caretIconStyle} />
121121
</div>
122122
</div>
123123
</Popover>

0 commit comments

Comments
 (0)