Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ export default function PivotTableChart(props: PivotTableProps) {
onContextMenu,
timeGrainSqla,
allowRenderHtml,
defaultRowExpansionDepth = 0,
defaultColExpansionDepth = 0,
} = props;

const theme = useTheme();
Expand Down Expand Up @@ -711,6 +713,8 @@ export default function PivotTableChart(props: PivotTableProps) {
namesMapping={verboseMap}
onContextMenu={handleContextMenu}
allowRenderHtml={allowRenderHtml}
defaultRowExpansionDepth={defaultRowExpansionDepth}
defaultColExpansionDepth={defaultColExpansionDepth}
Comment thread
robjuffermans marked this conversation as resolved.
/>
</PivotTableWrapper>
</Styles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,36 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'defaultRowExpansionDepth',
config: {
type: 'SelectControl',
label: t('Default row expansion depth'),
default: 0,
renderTrigger: true,
description: t(
'How many row group levels to show expanded on initial load. ' +
'Select "Fully expanded" (0) to show all levels. ' +
'Select "1" to show only top-level rows (children collapsed).',
),
mapStateToProps: explore => {
const rowCount = ensureIsArray(
explore?.controls?.groupbyRows?.value,
).length;
// 0 = fully expanded (no collapse), 1..N = collapse deeper levels
const choices: [number, string][] = [[0, t('Fully expanded')]];
for (let i = 1; i < rowCount; i += 1) {
choices.push([i, t('Expand %s level(s)', i)]);
}
return { choices };
},
visibility: ({ controls }) =>
!!controls?.rowSubTotals?.value &&
ensureIsArray(controls?.groupbyRows?.value).length > 1,
},
},
],
[
{
name: 'colTotals',
Expand All @@ -249,6 +279,36 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'defaultColExpansionDepth',
config: {
type: 'SelectControl',
label: t('Default column expansion depth'),
default: 0,
renderTrigger: true,
description: t(
'How many column group levels to show expanded on initial load. ' +
'Select "Fully expanded" (0) to show all levels. ' +
'Select "1" to show only top-level columns (children collapsed).',
),
mapStateToProps: explore => {
const colCount = ensureIsArray(
explore?.controls?.groupbyColumns?.value,
).length;
// 0 = fully expanded (no collapse), 1..N = collapse deeper levels
const choices: [number, string][] = [[0, t('Fully expanded')]];
for (let i = 1; i < colCount; i += 1) {
choices.push([i, t('Expand %s level(s)', i)]);
}
return { choices };
},
visibility: ({ controls }) =>
!!controls?.colSubTotals?.value &&
ensureIsArray(controls?.groupbyColumns?.value).length > 1,
},
},
],
[
{
name: 'transposePivot',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
timeGrainSqla,
currencyFormat,
allowRenderHtml,
defaultRowExpansionDepth = 0,
defaultColExpansionDepth = 0,
} = formData;
const { selectedFilters } = filterState;
const granularity = extractTimegrain(rawFormData);
Expand Down Expand Up @@ -195,5 +197,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
onContextMenu,
timeGrainSqla,
allowRenderHtml,
defaultRowExpansionDepth,
defaultColExpansionDepth,
Comment thread
robjuffermans marked this conversation as resolved.
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ interface TableRendererProps {
filters?: Record<string, string>,
) => void;
allowRenderHtml?: boolean;
defaultRowExpansionDepth?: number;
defaultColExpansionDepth?: number;
[key: string]: unknown;
}

Expand Down Expand Up @@ -134,6 +136,61 @@ interface PivotSettings {
colAttrSpans?: number[][];
}

/**
* Computes the initial collapsed-state map for a set of keys and a depth.
*
* Semantics:
* - depth = 0 (or invalid): disabled / fully expanded (returns an empty map)
* - depth = 1: collapse at level 1 (show only top-level rows/columns)
* - depth = N: collapse at level N (show N levels expanded)
*
* Every intermediate level from `depth` up to `maxDepth - 1` is collapsed, so
* expanding a group reveals only the next level while deeper levels stay
* collapsed until clicked. The leaf level (`maxDepth`) is never collapsed.
*
* @param keys - Array of key arrays (e.g. rowKeys or colKeys)
* @param depth - The (1-based) depth at which to collapse. Must be a positive
* integer (>= 1) to apply any collapse.
* @param maxDepth - Optional total number of grouping levels (e.g.
* rows.length or cols.length). Inferred from the longest key when omitted.
* @returns A map of flatKey => true for every key that should be collapsed.
*/
export function computeCollapsedMap(
keys: string[][],
depth?: number,
maxDepth?: number,
): Record<string, boolean> {
// depth must be a positive integer (>= 1); 0/invalid means fully expanded.
if (!Number.isInteger(depth) || (depth as number) <= 0) {
return {};
}
const collapseDepth = depth as number;

const effectiveMaxDepth = Number.isInteger(maxDepth)
? (maxDepth as number)
: Math.max(0, ...keys.map(k => (Array.isArray(k) ? k.length : 0)));
if (effectiveMaxDepth <= collapseDepth) {
return {};
}

const collapsed: Record<string, boolean> = {};
const seen = new Set<string>();
keys.forEach(k => {
if (!Array.isArray(k) || k.length < collapseDepth) {
return;
}
const limit = Math.min(k.length, effectiveMaxDepth - 1);
for (let i = collapseDepth; i <= limit; i += 1) {
const keyStr = flatKey(k.slice(0, i));
if (!seen.has(keyStr)) {
seen.add(keyStr);
collapsed[keyStr] = true;
}
}
});
return collapsed;
}

const parseLabel = (value: unknown): string | number => {
if (typeof value === 'string') {
if (value === 'metric') return t('metric');
Expand Down Expand Up @@ -338,6 +395,8 @@ export function TableRenderer(props: TableRendererProps) {
namesMapping: namesMappingProp,
onContextMenu,
allowRenderHtml,
defaultRowExpansionDepth = 0,
defaultColExpansionDepth = 0,
} = props;

const [collapsedRows, setCollapsedRows] = useState<Record<string, boolean>>(
Expand Down Expand Up @@ -743,6 +802,60 @@ export function TableRenderer(props: TableRendererProps) {
[getBasePivotSettings],
);

// Seed the initial collapsed state once, on mount, from the configured
// default expansion depths. Keeping this in an effect (rather than the render
// body or a state initializer) avoids the setState-during-render anti-pattern
// while still hiding deeper hierarchy levels on first load. A depth of 0 (or
// an invalid value) means "fully expanded" and collapses nothing.
const didApplyInitialCollapse = useRef(false);
useEffect(() => {
if (didApplyInitialCollapse.current) {
return;
}
didApplyInitialCollapse.current = true;

const rowDepth = Number(defaultRowExpansionDepth);
const colDepth = Number(defaultColExpansionDepth);
const hasValidRowDepth = Number.isInteger(rowDepth) && rowDepth > 0;
const hasValidColDepth = Number.isInteger(colDepth) && colDepth > 0;
if (!hasValidRowDepth && !hasValidColDepth) {
return;
}

const {
rowKeys,
colKeys,
rowAttrs,
colAttrs,
rowSubtotalDisplay,
colSubtotalDisplay,
} = basePivotSettings;

// Only collapse when subtotals are enabled and the requested depth sits
// above the deepest level (otherwise there is nothing to hide).
const initialCollapsedRows =
hasValidRowDepth &&
rowSubtotalDisplay.enabled &&
rowAttrs.length > 1 &&
rowDepth < rowAttrs.length
? computeCollapsedMap(rowKeys, rowDepth, rowAttrs.length)
: {};
const initialCollapsedCols =
hasValidColDepth &&
colSubtotalDisplay.enabled &&
colAttrs.length > 1 &&
colDepth < colAttrs.length
? computeCollapsedMap(colKeys, colDepth, colAttrs.length)
: {};

if (Object.keys(initialCollapsedRows).length > 0) {
setCollapsedRows(initialCollapsedRows);
}
if (Object.keys(initialCollapsedCols).length > 0) {
setCollapsedCols(initialCollapsedCols);
}
}, [basePivotSettings, defaultRowExpansionDepth, defaultColExpansionDepth]);

// Reset sort state and cache when structural props change. Scoping this to
// an effect (instead of running inside the memo) prevents the cache from
// being wiped on unrelated state updates like collapse/expand.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ interface PivotTableCustomizeProps {
time_grain_sqla?: TimeGranularity;
granularity_sqla?: string;
allowRenderHtml?: boolean;
defaultRowExpansionDepth?: number;
defaultColExpansionDepth?: number;
Comment thread
robjuffermans marked this conversation as resolved.
}

export type PivotTableQueryFormData = QueryFormData &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ test('should transform chart props for viz', () => {
columnFormats: {},
currencyFormats: {},
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
defaultRowExpansionDepth: 0,
defaultColExpansionDepth: 0,
});
});

Expand Down Expand Up @@ -283,6 +285,33 @@ test('should preserve static currency format when not using AUTO mode', () => {
});
});

test('should pass default row/column expansion depth through to the renderer', () => {
const expansionChartProps = new ChartProps<QueryFormData>({
formData: {
...formData,
defaultRowExpansionDepth: 2,
defaultColExpansionDepth: 1,
},
width: 800,
height: 600,
queriesData: [
{
data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
colnames: ['name', 'sum__num', '__timestamp'],
coltypes: [1, 0, 2],
},
],
hooks: { setDataMask },
filterState: { selectedFilters: {} },
datasource: { verboseMap: {}, columnFormats: {} },
theme: supersetTheme,
});

const result = transformProps(expansionChartProps);
expect(result.defaultRowExpansionDepth).toBe(2);
expect(result.defaultColExpansionDepth).toBe(1);
});

test('should map conditional formatting rules to metricColorFormatters with correct colors', () => {
const formattingFormData = {
...formData,
Expand Down
Loading
Loading