import type { DataType } from '@revolist/revogrid';
import {
ClientPivotEngineAdapter,
HttpPivotRemoteStore,
type PivotConfig,
type PivotDrilldownCellRef,
type PivotDrilldownRequest,
type PivotDrilldownResponse,
type PivotLoadRequest,
type PivotLoadResponse,
type PivotRemoteStore,
type PivotRemoteStoreHooks,
type PivotSummaryType,
type PivotStateResponse,
} from '@revolist/revogrid-enterprise';
/**
* Sample source rows used by the portal remote Pivot demo. The grid itself only
* sees the visible analytical block returned by the mocked remote API.
*/
export const PIVOT_REMOTE_ROWS: DataType[] = [
{ region: 'North', rep: 'Jane', year: 2024, quarter: 'Q1', sales: 32, margin: 8 },
{ region: 'North', rep: 'Jane', year: 2024, quarter: 'Q2', sales: 28, margin: 7 },
{ region: 'North', rep: 'John', year: 2024, quarter: 'Q1', sales: 24, margin: 5 },
{ region: 'North', rep: 'John', year: 2024, quarter: 'Q2', sales: 27, margin: 6 },
{ region: 'South', rep: 'Alice', year: 2024, quarter: 'Q1', sales: 22, margin: 4 },
{ region: 'South', rep: 'Alice', year: 2024, quarter: 'Q2', sales: 25, margin: 5 },
{ region: 'South', rep: 'Bob', year: 2024, quarter: 'Q1', sales: 18, margin: 3 },
{ region: 'South', rep: 'Bob', year: 2024, quarter: 'Q2', sales: 20, margin: 4 },
{ region: 'West', rep: 'Mia', year: 2025, quarter: 'Q1', sales: 29, margin: 6 },
{ region: 'West', rep: 'Mia', year: 2025, quarter: 'Q2', sales: 34, margin: 9 },
{ region: 'West', rep: 'Noah', year: 2025, quarter: 'Q1', sales: 21, margin: 4 },
{ region: 'West', rep: 'Noah', year: 2025, quarter: 'Q2', sales: 23, margin: 5 },
];
export const PIVOT_REMOTE_VIEW_ID = 'portal-sales-remote';
export const PIVOT_REMOTE_FIELDS_VERSION = 'portal:remote:v1';
/**
* Base Pivot configuration shared by all portal remote examples. The remote
* store is attached by each framework wrapper at runtime.
*/
export const PIVOT_REMOTE_CONFIG: Omit<PivotConfig, 'engine'> = {
dimensions: [
{ prop: 'region', name: 'Region' },
{ prop: 'rep', name: 'Rep' },
{ prop: 'year', name: 'Year' },
{ prop: 'quarter', name: 'Quarter' },
{ prop: 'sales', name: 'Sales' },
{ prop: 'margin', name: 'Margin' },
],
rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
filters: ['margin'],
totals: { grandTotal: true, subtotals: true },
collapsed: true,
columnCollapse: { collapsed: true },
groupAggregations: true,
};
export const PIVOT_REMOTE_SAVED_VIEW_PRESETS: Array<{
viewId: string;
name: string;
description: string;
config: Omit<PivotConfig, 'engine'>;
}> = [
{
viewId: 'north-sales-by-rep',
name: 'North Sales by Rep',
description: 'Region and rep rows with quarterly sales columns.',
config: {
...PIVOT_REMOTE_CONFIG,
rows: ['region', 'rep'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
filters: ['margin'],
expanded: { North: true },
},
},
{
viewId: 'quarterly-region-sales',
name: 'Quarterly Region Sales',
description: 'Region rows with year and quarter sales columns.',
config: {
...PIVOT_REMOTE_CONFIG,
rows: ['region'],
columns: ['year', 'quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
filters: ['rep', 'margin'],
expanded: {},
},
},
{
viewId: 'margin-by-region',
name: 'Margin by Region',
description: 'Region rows with yearly margin totals.',
config: {
...PIVOT_REMOTE_CONFIG,
rows: ['region'],
columns: ['year'],
values: [{ prop: 'margin', aggregator: 'sum' }],
filters: ['rep', 'quarter', 'sales'],
expanded: {},
},
},
];
export const PIVOT_REMOTE_DRILLDOWN_CELLS: Array<{
id: string;
label: string;
cell: PivotDrilldownCellRef;
}> = [
{
id: 'north-jane-2024-q1',
label: 'North / Jane / 2024 Q1',
cell: { rowPath: ['North', 'Jane'], columnPath: [2024, 'Q1'] },
},
{
id: 'south-alice-2024-q2',
label: 'South / Alice / 2024 Q2',
cell: { rowPath: ['South', 'Alice'], columnPath: [2024, 'Q2'] },
},
{
id: 'west-mia-2025-q2',
label: 'West / Mia / 2025 Q2',
cell: { rowPath: ['West', 'Mia'], columnPath: [2025, 'Q2'] },
},
];
export const PIVOT_REMOTE_DRILLDOWN_COLUMNS = ['region', 'rep', 'year', 'quarter', 'sales', 'margin'];
export interface PivotRemoteToolsActivity {
id: string;
label: string;
tone: 'neutral' | 'success' | 'warning';
}
type MockStateMap = Map<string, Record<string, unknown>>;
/**
* Builds a mock fetch implementation that exposes the same `/api/pivot/*`
* contract used by the real `HttpPivotRemoteStore`.
*/
export function createMockPivotFetch(
rows: DataType[] = PIVOT_REMOTE_ROWS,
baseConfig: Omit<PivotConfig, 'engine'> = PIVOT_REMOTE_CONFIG,
) {
const state = new Map<string, Record<string, unknown>>() as MockStateMap;
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === 'string'
? new URL(input, 'http://portal.local')
: new URL(input.toString(), 'http://portal.local');
const pathname = url.pathname;
const method = init?.method ?? 'GET';
if (pathname === '/api/pivot/load' && method === 'POST') {
const request = JSON.parse(String(init?.body ?? '{}')) as PivotLoadRequest;
const adapter = new ClientPivotEngineAdapter(createConfigFromLoadRequest(request, baseConfig), rows);
const response = await adapter.load(request);
return jsonResponse(response);
}
if (pathname === '/api/pivot/drilldown' && method === 'POST') {
const request = JSON.parse(String(init?.body ?? '{}')) as PivotDrilldownRequest;
const response = createMockDrilldownResponse(request, rows);
return jsonResponse(response);
}
if (pathname === '/api/pivot/state/save' && method === 'POST') {
const request = JSON.parse(String(init?.body ?? '{}')) as {
userId: string;
viewId: string;
state: Record<string, unknown>;
};
state.set(`${request.userId}:${request.viewId}`, request.state);
return new Response(null, { status: 204 });
}
if (pathname.startsWith('/api/pivot/state/') && method === 'GET') {
const [, , , userId, viewId] = pathname.split('/');
const saved = state.get(`${userId}:${viewId}`) ?? { expanded: { North: true } };
const response: PivotStateResponse = {
userId,
viewId,
state: saved,
version: 1,
};
return jsonResponse(response);
}
if (pathname === '/api/pivot/cache/invalidate' && method === 'POST') {
state.clear();
return new Response(null, { status: 204 });
}
return jsonResponse({ message: 'Not found' }, 404);
};
}
/**
* Creates the framework-agnostic remote store used by the live portal demos.
*/
export function createPivotRemoteStore(hooks?: PivotRemoteStoreHooks): PivotRemoteStore {
return new HttpPivotRemoteStore({
baseUrl: '',
tenantId: 'portal-demo',
datasetWatermark: 'portal-seed-v1',
authProvider: async () => ({
Authorization: 'Bearer portal-demo-token',
'x-tenant-id': 'portal-demo',
}),
fetchImpl: createMockPivotFetch() as typeof fetch,
hooks,
});
}
export function createPivotRemoteActivityHooks(
push: (activity: PivotRemoteToolsActivity) => void,
): PivotRemoteStoreHooks {
let seq = 0;
const next = (
label: string,
tone: PivotRemoteToolsActivity['tone'] = 'neutral',
): PivotRemoteToolsActivity => ({
id: `pivot-remote-activity-${++seq}`,
label,
tone,
});
return {
requestStarted: ({ type }) => push(next(`${type}: started`)),
requestSucceeded: ({ type, cacheStatus }) => {
push(next(`${type}: succeeded${cacheStatus ? ` (${cacheStatus})` : ''}`, 'success'));
},
requestFailed: ({ type }) => push(next(`${type}: failed`, 'warning')),
cacheStatusChanged: ({ cacheStatus }) => push(next(`cache: ${cacheStatus}`, 'neutral')),
};
}
export function withPivotRemoteEngine(
config: Partial<PivotConfig>,
remoteStore: PivotRemoteStore,
): Partial<PivotConfig> {
return {
...config,
engine: {
mode: 'server',
remoteStore,
viewId: PIVOT_REMOTE_VIEW_ID,
fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
rowAxis: { offset: 0, expandedPaths: [['North']] },
columnAxis: { offset: 0, limit: 8 },
},
};
}
export function withPivotRemoteToolsUi(config: Partial<PivotConfig>): Partial<PivotConfig> {
return {
...config,
fieldPanel: {
visible: true,
allowFieldDragging: true,
allowFieldRemoving: true,
showDataFields: true,
showRowFields: true,
showColumnFields: true,
showFilterFields: true,
...(typeof config.fieldPanel === 'object' ? config.fieldPanel : {}),
},
};
}
export function serializePivotRemoteToolsLayout(config: Partial<PivotConfig>): Partial<PivotConfig> {
const serializable = { ...config };
delete serializable.engine;
delete serializable.mountTo;
return {
...serializable,
fieldPanel: {
visible: true,
allowFieldDragging: true,
allowFieldRemoving: true,
showDataFields: true,
showRowFields: true,
showColumnFields: true,
showFilterFields: true,
...(typeof serializable.fieldPanel === 'object' ? serializable.fieldPanel : {}),
},
};
}
export function createPivotRemoteDiagnosticsRequest(
config: Partial<PivotConfig> = PIVOT_REMOTE_CONFIG,
): PivotLoadRequest {
return {
requestId: `pivot-tools-${Date.now()}`,
viewId: PIVOT_REMOTE_VIEW_ID,
fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
loadOptions: {
rows: (config.rows ?? []).map((selector) => ({ selector: String(selector) })),
columns: (config.columns ?? []).map((selector) => ({ selector: String(selector) })),
totalSummary: (config.values ?? []).map((value) => ({
selector: String(value.prop),
summaryType: value.aggregator as PivotSummaryType,
})),
groupSummary: (config.values ?? []).map((value) => ({
selector: String(value.prop),
summaryType: value.aggregator as PivotSummaryType,
})),
},
viewport: {
rowAxis: { offset: 0, limit: 6, expandedPaths: [['North']] },
columnAxis: { offset: 0, limit: 8 },
},
uiState: {
collapsedByDefault: config.collapsed ?? true,
rowHeaderLayout: 'tree',
},
};
}
function createConfigFromLoadRequest(
request: PivotLoadRequest,
baseConfig: Omit<PivotConfig, 'engine'>,
): Omit<PivotConfig, 'engine'> {
const values = request.loadOptions.totalSummary?.length
? request.loadOptions.totalSummary.map((summary) => ({
prop: summary.selector,
aggregator: summary.summaryType,
}))
: baseConfig.values;
return {
...baseConfig,
rows: request.loadOptions.rows?.map((row) => row.selector) ?? baseConfig.rows,
columns: request.loadOptions.columns?.map((column) => column.selector) ?? baseConfig.columns,
values,
collapsed: request.uiState?.collapsedByDefault ?? baseConfig.collapsed,
};
}
function createMockDrilldownResponse(
request: PivotDrilldownRequest,
rows: DataType[],
): PivotDrilldownResponse {
const [region, rep] = request.cell.rowPath;
const [year, quarter] = request.cell.columnPath;
const filtered = rows.filter((row) => {
return (region ? row.region === region : true)
&& (rep ? row.rep === rep : true)
&& (year ? row.year === year : true)
&& (quarter ? row.quarter === quarter : true);
});
const selectedColumns = request.customColumns?.length
? request.customColumns
: ['region', 'rep', 'year', 'quarter', 'sales'];
const page = filtered
.slice(request.offset, request.offset + request.limit)
.map((row) => Object.fromEntries(selectedColumns.map((column) => [column, row[column]])));
return {
requestId: request.requestId,
data: page,
totalCount: filtered.length,
meta: {
cacheStatus: 'bypass',
},
};
}
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: {
'content-type': 'application/json',
},
});
}