From 5512656ac8399e565665f4cb8831bc47514ba0cf Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 24 Dec 2025 15:57:36 +0530 Subject: [PATCH 1/2] feat: add show more for outermost pivot dimension --- .../dashboards/pivot/PivotDisplay.svelte | 2 + .../dashboards/pivot/PivotTable.svelte | 24 +++- .../dashboards/pivot/pivot-constants.ts | 8 ++ .../dashboards/pivot/pivot-data-store.ts | 65 +++++++++- .../features/dashboards/pivot/pivot-utils.ts | 9 +- .../pivot/tests/pivot-constants.test.ts | 122 ++++++++++++++++++ .../src/features/dashboards/pivot/types.ts | 5 +- .../dashboards/stores/dashboard-stores.ts | 11 ++ 8 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 web-common/src/features/dashboards/pivot/tests/pivot-constants.test.ts diff --git a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte index 9f8e9059cb5..5bc1c3658f5 100644 --- a/web-common/src/features/dashboards/pivot/PivotDisplay.svelte +++ b/web-common/src/features/dashboards/pivot/PivotDisplay.svelte @@ -150,6 +150,8 @@ rowId, columnId, )} + setPivotOutermostRowLimit={(limit) => + metricsExplorerStore.setPivotOutermostRowLimit($exploreName, limit)} setPivotRowLimitForExpanded={(expandIndex, limit) => metricsExplorerStore.setPivotRowLimitForExpandedRow( $exploreName, diff --git a/web-common/src/features/dashboards/pivot/PivotTable.svelte b/web-common/src/features/dashboards/pivot/PivotTable.svelte index 32c9351b9e7..a39405bbd6a 100644 --- a/web-common/src/features/dashboards/pivot/PivotTable.svelte +++ b/web-common/src/features/dashboards/pivot/PivotTable.svelte @@ -56,6 +56,8 @@ export let setPivotActiveCell: | ((rowId: string, columnId: string) => void) | undefined = undefined; + export let setPivotOutermostRowLimit: ((limit: number) => void) | undefined = + undefined; export let setPivotRowLimitForExpanded: | ((expandIndex: string, limit: number) => void) | undefined = undefined; @@ -209,13 +211,27 @@ // Handle "Show More" button clicks if (value === SHOW_MORE_BUTTON && rowHeader) { const rowData = row.original; - - const expandIndex = rowId.split(".").slice(0, -1).join("."); const currentLimit = rowData.__currentLimit as number; const nextLimit = getNextRowLimit(currentLimit); - if (expandIndex && nextLimit && setPivotRowLimitForExpanded) { - setPivotRowLimitForExpanded(expandIndex, nextLimit); + if (!nextLimit) return; + + // Check if this is the outermost dimension or a nested dimension + // Outermost dimension has rowId like "0", "1", etc. (no dots) + // Nested dimensions have rowId like "0.1", "0.1.2", etc. + const isOutermostDimension = !rowId.includes("."); + + if (isOutermostDimension) { + // Handle outermost dimension "Show more" click + if (setPivotOutermostRowLimit) { + setPivotOutermostRowLimit(nextLimit); + } + } else { + // Handle nested dimension "Show more" click + const expandIndex = rowId.split(".").slice(0, -1).join("."); + if (expandIndex && setPivotRowLimitForExpanded) { + setPivotRowLimitForExpanded(expandIndex, nextLimit); + } } return; } diff --git a/web-common/src/features/dashboards/pivot/pivot-constants.ts b/web-common/src/features/dashboards/pivot/pivot-constants.ts index c4a939bdd9a..465600d4d9b 100644 --- a/web-common/src/features/dashboards/pivot/pivot-constants.ts +++ b/web-common/src/features/dashboards/pivot/pivot-constants.ts @@ -2,6 +2,7 @@ export const SHOW_MORE_BUTTON = "__rill_type_SHOW_MORE_BUTTON"; export const LOADING_CELL = "__rill_type_LOADING_CELL"; export const MAX_ROW_EXPANSION_LIMIT = 100; +export const ROW_LIMIT_INCREMENT_AFTER_100 = 50; /** * Allowed row limit values for the pivot table. @@ -17,12 +18,14 @@ export const PIVOT_ROW_LIMIT_OPTIONS = [5, 10, 25, 50, 100] as const; * @param rowLimit - The maximum number of rows to fetch (undefined = unlimited) * @param rowOffset - The current row offset (for pagination) * @param pageSize - The number of rows per page + * @param respectPageSize - Whether to constrain by page size (default: true). Set to false for explicit user-requested limits. * @returns The limit to apply as a string for the query */ export function calculateEffectiveRowLimit( rowLimit: number | undefined, rowOffset: number, pageSize: number, + respectPageSize: boolean = true, ): string { if (rowLimit === undefined) { return pageSize.toString(); @@ -31,6 +34,11 @@ export function calculateEffectiveRowLimit( if (remainingRows <= 0) { return "0"; } + // When respectPageSize is false (e.g., for explicit user-requested limits like "Show more" button), + // don't constrain by page size to allow fetching the full requested amount + if (!respectPageSize) { + return remainingRows.toString(); + } return Math.min(remainingRows, pageSize).toString(); } diff --git a/web-common/src/features/dashboards/pivot/pivot-data-store.ts b/web-common/src/features/dashboards/pivot/pivot-data-store.ts index 2fb93144dac..662d87ccf06 100644 --- a/web-common/src/features/dashboards/pivot/pivot-data-store.ts +++ b/web-common/src/features/dashboards/pivot/pivot-data-store.ts @@ -1,5 +1,9 @@ import { getDimensionFilterWithSearch } from "@rilldata/web-common/features/dashboards/dimension-table/dimension-table-utils"; -import { calculateEffectiveRowLimit } from "@rilldata/web-common/features/dashboards/pivot/pivot-constants"; +import { + calculateEffectiveRowLimit, + MAX_ROW_EXPANSION_LIMIT, + SHOW_MORE_BUTTON, +} from "@rilldata/web-common/features/dashboards/pivot/pivot-constants"; import { mergeFilters } from "@rilldata/web-common/features/dashboards/pivot/pivot-merge-filters"; import { memoizeMetricsStore } from "@rilldata/web-common/features/dashboards/state-managers/memoize-metrics-store"; import type { StateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers"; @@ -299,13 +303,27 @@ export function createPivotDataStore( readable(null); if (!isFlat) { - // Calculate the effective limit based on rowLimit, offset, and page size + // Use outermostRowLimit if set, otherwise fall back to rowLimit + const effectiveOutermostLimit = + config.pivot.outermostRowLimit ?? config.pivot.rowLimit; + + // Calculate the effective limit based on outermostRowLimit, offset, and page size + // When outermostRowLimit is explicitly set, don't constrain by page size + const isExplicitOutermostLimit = + config.pivot.outermostRowLimit !== undefined; const limitToApply = calculateEffectiveRowLimit( - config.pivot.rowLimit, + effectiveOutermostLimit, rowOffset, NUM_ROWS_PER_PAGE, + !isExplicitOutermostLimit, // Don't respect page size for explicit outermost limit ); + // Query for limit + 1 to detect if there's more data + const limitToQuery = + effectiveOutermostLimit !== undefined + ? (parseInt(limitToApply) + 1).toString() + : limitToApply; + // Get sort order for the anchor dimension rowDimensionAxisQuery = getAxisForDimensions( ctx, @@ -315,7 +333,7 @@ export function createPivotDataStore( whereFilter, sortPivotBy, timeRange, - limitToApply, + limitToQuery, rowOffset.toString(), ); } @@ -424,9 +442,30 @@ export function createPivotDataStore( globalTotalsResponse?.data?.data, ); - const rowDimensionValues = + let rowDimensionValues = rowDimensionAxes?.data?.[anchorDimension] || []; + // Detect if there's more data for the outermost dimension + // and trim to the actual limit + let hasMoreRows = false; + const effectiveOutermostLimit = + config.pivot.outermostRowLimit ?? config.pivot.rowLimit; + if (!isFlat && effectiveOutermostLimit !== undefined) { + const isExplicitOutermostLimit = + config.pivot.outermostRowLimit !== undefined; + const limitToApply = calculateEffectiveRowLimit( + effectiveOutermostLimit, + rowOffset, + NUM_ROWS_PER_PAGE, + !isExplicitOutermostLimit, // Don't respect page size for explicit outermost limit + ); + const actualLimit = parseInt(limitToApply); + if (rowDimensionValues.length > actualLimit) { + hasMoreRows = true; + rowDimensionValues = rowDimensionValues.slice(0, actualLimit); + } + } + const totalColumns = getTotalColumnCount(totalsRowData); const axesRowTotals = @@ -605,6 +644,22 @@ export function createPivotDataStore( expandedTableMap[key] = tableDataExpanded; } + // Add "Show more" row for outermost dimension if needed + const effectiveOutermostLimit = + config.pivot.outermostRowLimit ?? config.pivot.rowLimit; + + if ( + hasMoreRows && + effectiveOutermostLimit && + effectiveOutermostLimit < MAX_ROW_EXPANSION_LIMIT + ) { + const showMoreRow: PivotDataRow = { + [anchorDimension]: SHOW_MORE_BUTTON, + __currentLimit: effectiveOutermostLimit, + } as PivotDataRow; + tableDataExpanded = [...tableDataExpanded, showMoreRow]; + } + const activeCell = config.pivot.activeCell; let activeCellFilters: PivotFilter | undefined = undefined; if (activeCell) { diff --git a/web-common/src/features/dashboards/pivot/pivot-utils.ts b/web-common/src/features/dashboards/pivot/pivot-utils.ts index 680fc3f0644..fd71b2eec73 100644 --- a/web-common/src/features/dashboards/pivot/pivot-utils.ts +++ b/web-common/src/features/dashboards/pivot/pivot-utils.ts @@ -54,7 +54,12 @@ export function getPivotConfigKey(config: PivotDataStoreConfig) { pivot, } = config; - const { sorting, tableMode: tableModeKey, rowLimit } = pivot; + const { + sorting, + tableMode: tableModeKey, + rowLimit, + outermostRowLimit, + } = pivot; const timeKey = JSON.stringify(time); const sortingKey = JSON.stringify(sorting); const filterKey = JSON.stringify(whereFilter); @@ -63,7 +68,7 @@ export function getPivotConfigKey(config: PivotDataStoreConfig) { .concat(measureNames, colDimensionNames) .join("_"); - return `${dimsAndMeasures}_${timeKey}_${sortingKey}_${tableModeKey}_${filterKey}_${enableComparison}_${comparisonTimeKey}_${rowLimit ?? "all"}`; + return `${dimsAndMeasures}_${timeKey}_${sortingKey}_${tableModeKey}_${filterKey}_${enableComparison}_${comparisonTimeKey}_${rowLimit ?? "all"}_${outermostRowLimit ?? "none"}`; } /** diff --git a/web-common/src/features/dashboards/pivot/tests/pivot-constants.test.ts b/web-common/src/features/dashboards/pivot/tests/pivot-constants.test.ts new file mode 100644 index 00000000000..3380a51898a --- /dev/null +++ b/web-common/src/features/dashboards/pivot/tests/pivot-constants.test.ts @@ -0,0 +1,122 @@ +import { + calculateEffectiveRowLimit, + getNextRowLimit, + getNextLimitLabel, +} from "@rilldata/web-common/features/dashboards/pivot/pivot-constants"; +import { describe, it, expect } from "vitest"; + +describe("calculateEffectiveRowLimit", () => { + describe("with respectPageSize=true (default)", () => { + it("returns pageSize when rowLimit is undefined", () => { + expect(calculateEffectiveRowLimit(undefined, 0, 50)).toBe("50"); + expect(calculateEffectiveRowLimit(undefined, 10, 50)).toBe("50"); + }); + + it("returns 0 when remainingRows is 0 or negative", () => { + expect(calculateEffectiveRowLimit(50, 50, 50)).toBe("0"); + expect(calculateEffectiveRowLimit(50, 60, 50)).toBe("0"); + }); + + it("returns min of remainingRows and pageSize when both are positive", () => { + // remainingRows < pageSize + expect(calculateEffectiveRowLimit(30, 0, 50)).toBe("30"); + expect(calculateEffectiveRowLimit(100, 80, 50)).toBe("20"); + + // remainingRows > pageSize + expect(calculateEffectiveRowLimit(100, 0, 50)).toBe("50"); + expect(calculateEffectiveRowLimit(150, 50, 50)).toBe("50"); + + // remainingRows == pageSize + expect(calculateEffectiveRowLimit(50, 0, 50)).toBe("50"); + }); + + it("handles pagination correctly with rowOffset", () => { + const rowLimit = 100; + const pageSize = 50; + + // First page + expect(calculateEffectiveRowLimit(rowLimit, 0, pageSize)).toBe("50"); + + // Second page + expect(calculateEffectiveRowLimit(rowLimit, 50, pageSize)).toBe("50"); + + // Third page (only 10 rows left) + expect(calculateEffectiveRowLimit(rowLimit, 90, pageSize)).toBe("10"); + }); + }); + + describe("with respectPageSize=false", () => { + it("returns pageSize when rowLimit is undefined", () => { + expect(calculateEffectiveRowLimit(undefined, 0, 50, false)).toBe("50"); + }); + + it("returns 0 when remainingRows is 0 or negative", () => { + expect(calculateEffectiveRowLimit(50, 50, 50, false)).toBe("0"); + expect(calculateEffectiveRowLimit(50, 60, 50, false)).toBe("0"); + }); + + it("returns full remainingRows without pageSize constraint", () => { + // This is the key difference - no min() with pageSize + expect(calculateEffectiveRowLimit(100, 0, 50, false)).toBe("100"); + expect(calculateEffectiveRowLimit(75, 0, 50, false)).toBe("75"); + expect(calculateEffectiveRowLimit(150, 0, 50, false)).toBe("150"); + }); + + it("handles offset correctly without pageSize constraint", () => { + expect(calculateEffectiveRowLimit(100, 20, 50, false)).toBe("80"); + expect(calculateEffectiveRowLimit(100, 50, 50, false)).toBe("50"); + expect(calculateEffectiveRowLimit(100, 90, 50, false)).toBe("10"); + }); + + it("allows fetching more than one page at once", () => { + // When user clicks "Show more" to 100, we want to fetch all 100 rows + // not just 50 (one page) + expect(calculateEffectiveRowLimit(100, 0, 50, false)).toBe("100"); + }); + }); +}); + +describe("getNextRowLimit", () => { + it("returns next limit in progression: 5 → 10 → 25 → 50 → 100", () => { + expect(getNextRowLimit(5)).toBe(10); + expect(getNextRowLimit(10)).toBe(25); + expect(getNextRowLimit(25)).toBe(50); + expect(getNextRowLimit(50)).toBe(100); + }); + + it("returns undefined when at or beyond 100", () => { + expect(getNextRowLimit(100)).toBeUndefined(); + expect(getNextRowLimit(150)).toBeUndefined(); + }); + + it("finds next higher value when current limit is not in progression", () => { + expect(getNextRowLimit(7)).toBe(10); + expect(getNextRowLimit(15)).toBe(25); + expect(getNextRowLimit(30)).toBe(50); + expect(getNextRowLimit(60)).toBe(100); + }); + + it("handles edge cases", () => { + expect(getNextRowLimit(1)).toBe(5); + expect(getNextRowLimit(99)).toBe(100); + }); +}); + +describe("getNextLimitLabel", () => { + it("returns string representation of next limit", () => { + expect(getNextLimitLabel(5)).toBe("10"); + expect(getNextLimitLabel(10)).toBe("25"); + expect(getNextLimitLabel(25)).toBe("50"); + expect(getNextLimitLabel(50)).toBe("100"); + }); + + it("returns '100' when at or beyond max limit", () => { + expect(getNextLimitLabel(100)).toBe("100"); + expect(getNextLimitLabel(150)).toBe("100"); + }); + + it("handles non-standard limits", () => { + expect(getNextLimitLabel(7)).toBe("10"); + expect(getNextLimitLabel(30)).toBe("50"); + }); +}); diff --git a/web-common/src/features/dashboards/pivot/types.ts b/web-common/src/features/dashboards/pivot/types.ts index 9f988dcb841..718da785649 100644 --- a/web-common/src/features/dashboards/pivot/types.ts +++ b/web-common/src/features/dashboards/pivot/types.ts @@ -55,8 +55,9 @@ export interface PivotState { enableComparison: boolean; tableMode: PivotTableMode; activeCell: PivotCell | null; - rowLimit?: number; // Global limit - nestedRowLimits?: Record; // Per-row limits keyed by expand index (e.g., "0.1.2") + rowLimit?: number; + outermostRowLimit?: number; // Local limit for outermost dimension only + nestedRowLimits?: Record; // Local per-row limits keyed by expand index (e.g., "0.1.2") } export type PivotTableMode = "flat" | "nest"; diff --git a/web-common/src/features/dashboards/stores/dashboard-stores.ts b/web-common/src/features/dashboards/stores/dashboard-stores.ts index 3069d869b8f..6d4dec7e726 100644 --- a/web-common/src/features/dashboards/stores/dashboard-stores.ts +++ b/web-common/src/features/dashboards/stores/dashboard-stores.ts @@ -552,6 +552,7 @@ const metricsViewReducers = { rowLimit: limit, expanded: {}, nestedRowLimits: {}, + outermostRowLimit: undefined, rowPage: 1, activeCell: null, }; @@ -574,6 +575,16 @@ const metricsViewReducers = { }; }); }, + + setPivotOutermostRowLimit(name: string, limit: number) { + updateMetricsExplorerByName(name, (exploreState) => { + exploreState.pivot = { + ...exploreState.pivot, + outermostRowLimit: limit, + activeCell: null, + }; + }); + }, }; export const metricsExplorerStore: Readable & From 4a3f0fa24a2d3c3422dd85133d4f30fb5fa95676 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Wed, 24 Dec 2025 16:24:41 +0530 Subject: [PATCH 2/2] alignment and state fix --- .../features/dashboards/pivot/PivotExpandableCell.svelte | 5 ++++- .../src/features/dashboards/pivot/PivotShowMoreCell.svelte | 7 ++++++- .../features/dashboards/pivot/pivot-column-definition.ts | 5 +++++ .../src/features/dashboards/pivot/pivot-constants.ts | 1 - .../src/features/dashboards/stores/dashboard-stores.ts | 5 ++++- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte b/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte index 39a7c4d5813..019c247ea11 100644 --- a/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte +++ b/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte @@ -8,10 +8,13 @@ export let row: Row; export let value: string; export let assembled = true; + export let hasNestedDimensions = false; $: canExpand = row.getCanExpand(); $: expanded = row.getIsExpanded(); $: assembledAndCanExpand = assembled && canExpand; + + $: needsSpacer = row.depth >= 1 || (hasNestedDimensions && !canExpand);
- {:else if row.depth >= 1} + {:else if needsSpacer} {/if} diff --git a/web-common/src/features/dashboards/pivot/PivotShowMoreCell.svelte b/web-common/src/features/dashboards/pivot/PivotShowMoreCell.svelte index a6bfca9c858..3ed3d8b497b 100644 --- a/web-common/src/features/dashboards/pivot/PivotShowMoreCell.svelte +++ b/web-common/src/features/dashboards/pivot/PivotShowMoreCell.svelte @@ -8,10 +8,15 @@ export let row: Row; export let value: string; export let assembled = true; + export let hasNestedDimensions = false; + + $: needsSpacer = row.depth >= 1 || hasNestedDimensions;
- + {#if needsSpacer} + + {/if} Show more ... diff --git a/web-common/src/features/dashboards/pivot/pivot-column-definition.ts b/web-common/src/features/dashboards/pivot/pivot-column-definition.ts index 131dd9f13a3..ad452a75ec6 100644 --- a/web-common/src/features/dashboards/pivot/pivot-column-definition.ts +++ b/web-common/src/features/dashboards/pivot/pivot-column-definition.ts @@ -429,6 +429,9 @@ function getNestedColumnDef( const rowDimensionsForColumnDef = rowDimensions.slice(0, 1); const nestedLabel = getRowNestedLabel(rowDimensions); + // Check if there are nested dimensions (more than one row dimension) + const hasNestedDimensions = rowDimensions.length > 1; + // Create row dimension columns const rowDefinitions: ColumnDef[] = rowDimensionsForColumnDef.map((d) => { @@ -450,6 +453,7 @@ function getNestedColumnDef( return cellComponent(PivotShowMoreCell, { value: label, row, + hasNestedDimensions, }); } @@ -463,6 +467,7 @@ function getNestedColumnDef( return cellComponent(PivotExpandableCell, { value: formattedDimensionValue, row, + hasNestedDimensions, }); }, }; diff --git a/web-common/src/features/dashboards/pivot/pivot-constants.ts b/web-common/src/features/dashboards/pivot/pivot-constants.ts index 465600d4d9b..11b78258857 100644 --- a/web-common/src/features/dashboards/pivot/pivot-constants.ts +++ b/web-common/src/features/dashboards/pivot/pivot-constants.ts @@ -2,7 +2,6 @@ export const SHOW_MORE_BUTTON = "__rill_type_SHOW_MORE_BUTTON"; export const LOADING_CELL = "__rill_type_LOADING_CELL"; export const MAX_ROW_EXPANSION_LIMIT = 100; -export const ROW_LIMIT_INCREMENT_AFTER_100 = 50; /** * Allowed row limit values for the pivot table. diff --git a/web-common/src/features/dashboards/stores/dashboard-stores.ts b/web-common/src/features/dashboards/stores/dashboard-stores.ts index 6d4dec7e726..b25618ae3ee 100644 --- a/web-common/src/features/dashboards/stores/dashboard-stores.ts +++ b/web-common/src/features/dashboards/stores/dashboard-stores.ts @@ -261,6 +261,7 @@ const metricsViewReducers = { } } + exploreState.pivot.expanded = {}; exploreState.pivot.rows = dimensions; }); }, @@ -269,7 +270,6 @@ const metricsViewReducers = { updateMetricsExplorerByName(name, (exploreState) => { exploreState.pivot.rowPage = 1; exploreState.pivot.activeCell = null; - exploreState.pivot.expanded = {}; if (exploreState.pivot.sorting.length) { const accessor = exploreState.pivot.sorting[0].id; @@ -286,6 +286,7 @@ const metricsViewReducers = { } } } + exploreState.pivot.expanded = {}; exploreState.pivot.columns = value; }); }, @@ -294,6 +295,8 @@ const metricsViewReducers = { updateMetricsExplorerByName(name, (exploreState) => { exploreState.pivot.rowPage = 1; exploreState.pivot.activeCell = null; + exploreState.pivot.expanded = {}; + if (value.type === PivotChipType.Measure) { exploreState.pivot.columns.push(value); } else {