Skip to content

Remote Tools

Remote tools are small UI and application-layer helpers for server-side Pivot. They help you show request diagnostics, load source rows for a summarized cell, track remote activity, and manage saved Pivot layouts that can be restored into the grid.

These helpers are not a required backend. They sit around a PivotRemoteStore or PivotEngineAdapter that your app already uses for remote Pivot loading. In the demo, the remote tools panel is outside the dedicated RevoGrid wrapper so Pivot-owned UI such as the field panel can mount directly beside the grid.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import type {
  PivotConfig,
  PivotDiagnosticsModel,
  PivotDrilldownState,
  PivotSavedViewRecord,
} from '@revolist/revogrid-enterprise';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  PivotDrilldownController,
  PivotPlugin,
  createPivotDiagnosticsModel,
  deletePivotSavedView,
  duplicatePivotSavedView,
  loadPivotView,
  renamePivotSavedView,
  savePivotView,
} from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_REMOTE_DRILLDOWN_CELLS,
  PIVOT_REMOTE_DRILLDOWN_COLUMNS,
  PIVOT_REMOTE_FIELDS_VERSION,
  PIVOT_REMOTE_SAVED_VIEW_PRESETS,
  PIVOT_REMOTE_VIEW_ID,
  createPivotRemoteActivityHooks,
  createPivotRemoteDiagnosticsRequest,
  createPivotRemoteStore,
  serializePivotRemoteToolsLayout,
  type PivotRemoteToolsActivity,
  withPivotRemoteEngine,
  withPivotRemoteToolsUi,
} from '../sys-data/pivot.remote';

const USER_ID = 'portal-demo-user';

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) {
    return () => undefined;
  }

  const { isDark } = currentTheme();
  const activity: PivotRemoteToolsActivity[] = [];
  let diagnostics: PivotDiagnosticsModel | null = null;
  let drilldownState: PivotDrilldownState;
  let views: PivotSavedViewRecord[] = [];
  let activeViewId = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId;
  let viewName = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name;
  let activeDrilldownId = PIVOT_REMOTE_DRILLDOWN_CELLS[0].id;
  let currentLayoutConfig: Partial<PivotConfig> = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config;
  let message = 'Use the grid field panel to change dimensions, then save the layout.';

  const store = createPivotRemoteStore(createPivotRemoteActivityHooks((entry) => {
    activity.unshift(entry);
    activity.splice(8);
    render();
  }));
  const drilldown = new PivotDrilldownController(store, { defaultLimit: 5 });
  drilldownState = drilldown.getState();
  const createGridPivot = (config: Partial<PivotConfig>) => withPivotRemoteEngine(withPivotRemoteToolsUi(config), store);

  const root = document.createElement('div');
  root.className = 'flex h-full min-h-[620px] gap-3';

  const panel = document.createElement('aside');
  panel.className = 'w-[320px] shrink-0 space-y-3 overflow-auto border-r border-gray-200 pr-3 text-sm dark:border-gray-700';

  const gridShell = document.createElement('div');
  gridShell.className = 'grid-shell h-full min-w-0 grow overflow-hidden';

  const grid = document.createElement('revo-grid');
  grid.className = 'h-full w-full cell-border';
  grid.range = true;
  grid.resize = true;
  grid.readonly = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.colSize = 180;
  grid.columnTypes = {
    currency: new NumberColumnType('$0,0.00'),
  };
  grid.plugins = [PivotPlugin, PaginationPlugin, RowOddPlugin] as any;
  Object.assign(grid, {
    pivot: createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config),
    pagination: {
      itemsPerPage: 3,
      initialPage: 0,
      total: 0,
    },
  });

  const unsubscribe = drilldown.subscribe((state) => {
    drilldownState = state;
    render();
  });

  function render() {
    const activeViews = getActiveViews();
    const customViews = activeViews.filter((view) => !getPreset(view.viewId));
    const activePreset = getPreset(activeViewId);
    panel.innerHTML = `
      <h3 class="m-0 text-base font-semibold">Remote Pivot tools</h3>
      <p class="m-0 text-xs text-gray-500">${message}</p>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Saved layouts</h4>
        <select data-action="select-view" class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900">
          ${PIVOT_REMOTE_SAVED_VIEW_PRESETS.map((preset) => `<option value="${preset.viewId}" ${preset.viewId === activeViewId ? 'selected' : ''}>${preset.name}</option>`).join('')}
          ${customViews.map((view) => `<option value="${view.viewId}" ${view.viewId === activeViewId ? 'selected' : ''}>${getViewName(view)}</option>`).join('')}
        </select>
        <p class="m-0 text-xs text-gray-500">${activePreset?.description ?? 'Saved field layout from the grid panel.'}</p>
        <div class="flex flex-wrap gap-1">
          <button data-action="save" class="rounded border px-2 py-1">Save current layout</button>
          <button data-action="rename" class="rounded border px-2 py-1">Rename</button>
          <button data-action="duplicate" class="rounded border px-2 py-1">Duplicate</button>
          <button data-action="delete" class="rounded border px-2 py-1">Delete</button>
        </div>
        <div class="max-h-24 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
          ${activeViews.map((view) => `<div>${view.viewId}: ${getViewName(view)}</div>`).join('') || 'Save a preset to create a user-owned view.'}
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Drilldown</h4>
        <select data-action="select-drilldown" class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900">
          ${PIVOT_REMOTE_DRILLDOWN_CELLS.map((item) => `<option value="${item.id}" ${item.id === activeDrilldownId ? 'selected' : ''}>${item.label}</option>`).join('')}
        </select>
        <div class="flex flex-wrap gap-1">
          <button data-action="drilldown" class="rounded border px-2 py-1">Load source rows</button>
          <button data-action="reset-drilldown" class="rounded border px-2 py-1">Reset</button>
        </div>
        <div class="text-xs text-gray-600 dark:text-gray-300">
          ${drilldownState.status} - ${drilldownState.rows.length} of ${drilldownState.totalCount} rows - ${drilldownState.columns.length} columns
        </div>
        <div class="max-h-36 overflow-auto rounded border border-gray-200 dark:border-gray-700">
          ${renderDrilldownTable(drilldownState)}
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Diagnostics</h4>
        <button data-action="diagnostics" class="rounded border px-2 py-1">Run diagnostics</button>
        <div class="space-y-1 text-xs text-gray-600 dark:text-gray-300">
          ${diagnostics?.rows.map((row) => `<div>${row.label}: ${row.value}</div>`).join('') ?? 'No diagnostics yet'}
        </div>
        <div class="flex flex-wrap gap-1">
          ${diagnostics?.chips.map((chip) => `<span class="rounded border px-2 py-0.5 text-xs ${chipClass(chip.tone)}">${chip.label}</span>`).join('') ?? ''}
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Remote activity</h4>
        <div class="max-h-28 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
          ${activity.map((entry) => `<div class="${activityClass(entry.tone)}">${entry.label}</div>`).join('') || 'No remote activity yet.'}
        </div>
      </section>
    `;
  }

  async function runDiagnostics() {
    const response = await store.load(createPivotRemoteDiagnosticsRequest(getCurrentLayoutConfig()));
    diagnostics = createPivotDiagnosticsModel(response, {
      formatGeneratedAt: (value) => new Date(value).toLocaleTimeString(),
    });
    message = 'Diagnostics refreshed for the selected layout.';
    render();
  }

  async function loadFacts() {
    const activeDrilldown = getActiveDrilldown();
    await drilldown.load({
      viewId: PIVOT_REMOTE_VIEW_ID,
      fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
      cell: activeDrilldown.cell,
      customColumns: PIVOT_REMOTE_DRILLDOWN_COLUMNS,
    });
    message = `Loaded source rows for ${activeDrilldown.label}.`;
    render();
  }

  async function saveView() {
    const preset = getPreset(activeViewId);
    const targetViewId = preset ? `custom-layout-${getActiveViews().length + 1}` : activeViewId;
    const targetName = preset ? `Saved Layout ${getActiveViews().length + 1}` : viewName;
    const saved = await savePivotView(store, {
      userId: USER_ID,
      viewId: targetViewId,
      name: targetName,
      config: getCurrentLayoutConfig(),
    });
    views = [...views.filter((view) => view.viewId !== saved.viewId), saved];
    activeViewId = saved.viewId;
    viewName = saved.name ?? saved.viewId;
    currentLayoutConfig = saved.config;
    Object.assign(grid, { pivot: createGridPivot(saved.config) });
    message = `Saved ${saved.name ?? saved.viewId}.`;
    render();
  }

  panel.addEventListener('change', (event) => {
    const target = event.target as HTMLSelectElement;
    if (target.dataset.action === 'select-view') {
      void selectView(target.value);
    }
    if (target.dataset.action === 'select-drilldown') {
      activeDrilldownId = target.value;
      render();
    }
  });

  panel.addEventListener('click', (event) => {
    const action = (event.target as HTMLElement).dataset.action;
    if (!action) return;

    void (async () => {
      if (action === 'diagnostics') {
        await runDiagnostics();
      } else if (action === 'drilldown') {
        await loadFacts();
      } else if (action === 'reset-drilldown') {
        drilldown.reset();
        message = 'Drilldown reset.';
        render();
      } else if (action === 'save') {
        await saveView();
      } else if (action === 'rename') {
        renameView();
      } else if (action === 'duplicate') {
        duplicateView();
      } else if (action === 'delete') {
        deleteView();
      }
    })().catch((error) => {
      message = error instanceof Error ? error.message : String(error);
      render();
    });
  });

  function renameView() {
    if (!hasActiveView(views, activeViewId)) {
      message = 'Save a view before renaming.';
      render();
      return;
    }
    const nextName = `${viewName} Renamed`;
    views = renamePivotSavedView(views, { userId: USER_ID, viewId: activeViewId, name: nextName });
    viewName = nextName;
    message = `Renamed ${activeViewId}.`;
    render();
  }

  async function selectView(viewId: string) {
    activeViewId = viewId;
    const preset = getPreset(viewId);
    if (preset) {
      viewName = preset.name;
      currentLayoutConfig = preset.config;
      Object.assign(grid, { pivot: createGridPivot(preset.config) });
      message = `Loaded ${preset.name} preset into the grid.`;
      render();
      return;
    }

    const saved = await loadPivotView(store, { userId: USER_ID, viewId });
    viewName = saved.name ?? saved.viewId;
    currentLayoutConfig = saved.config;
    Object.assign(grid, { pivot: createGridPivot(saved.config) });
    message = `Loaded ${saved.name ?? saved.viewId} into the grid.`;
    render();
  }

  function duplicateView() {
    if (!hasActiveView(views, activeViewId)) {
      message = 'Save a view before duplicating.';
      render();
      return;
    }
    const newViewId = `${activeViewId}-copy-${getActiveViews().length + 1}`;
    const nextName = `${viewName} Copy`;
    views = duplicatePivotSavedView(views, {
      userId: USER_ID,
      viewId: activeViewId,
      newViewId,
      name: nextName,
    });
    activeViewId = newViewId;
    viewName = nextName;
    const duplicate = getActiveViews().find((view) => view.viewId === newViewId);
    if (duplicate && 'config' in duplicate) {
      currentLayoutConfig = duplicate.config;
      Object.assign(grid, { pivot: createGridPivot(duplicate.config) });
    }
    message = `Duplicated as ${newViewId}.`;
    render();
  }

  function deleteView() {
    if (!hasActiveView(views, activeViewId)) {
      message = 'Save a view before deleting.';
      render();
      return;
    }
    views = deletePivotSavedView(views, { userId: USER_ID, viewId: activeViewId });
    activeViewId = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId;
    viewName = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name;
    currentLayoutConfig = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config;
    Object.assign(grid, { pivot: createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config) });
    message = 'Deleted active view.';
    render();
  }

  function getCurrentLayoutConfig(): Partial<PivotConfig> {
    return serializePivotRemoteToolsLayout(currentLayoutConfig);
  }

  function getActiveViews() {
    return views.filter((view) => !('deleted' in view));
  }

  function getActiveDrilldown() {
    return PIVOT_REMOTE_DRILLDOWN_CELLS.find((item) => item.id === activeDrilldownId) ?? PIVOT_REMOTE_DRILLDOWN_CELLS[0];
  }

  render();
  gridShell.append(grid);
  root.append(panel, gridShell);
  parent.appendChild(root);
  grid.source = [];
  const onPivotConfigUpdate = (event: Event) => {
    currentLayoutConfig = serializePivotRemoteToolsLayout((event as CustomEvent<PivotConfig>).detail ?? currentLayoutConfig);
    Object.assign(grid, { pivot: createGridPivot(currentLayoutConfig) });
    message = 'Layout changed in the field panel. Save it to keep this arrangement.';
    render();
  };
  grid.addEventListener('pivot-config-update', onPivotConfigUpdate);
  void runDiagnostics();

  return () => {
    unsubscribe();
    grid.removeEventListener('pivot-config-update', onPivotConfigUpdate);
    drilldown.abort();
    root.remove();
  };
}

function renderDrilldownTable(state: PivotDrilldownState) {
  const headers = state.columns.map((column) => {
    return `<th class="border-b px-2 py-1 text-left font-semibold dark:border-gray-700">${column.name ?? String(column.prop)}</th>`;
  }).join('');
  const rows = state.rows.map((row) => {
    const cells = state.columns.map((column) => {
      return `<td class="border-b px-2 py-1 dark:border-gray-800">${String(row[column.prop] ?? '')}</td>`;
    }).join('');
    return `<tr>${cells}</tr>`;
  }).join('');
  const empty = state.rows.length
    ? ''
    : `<tr><td class="px-2 py-2 text-gray-500" colspan="${Math.max(state.columns.length, 1)}">No source rows loaded.</td></tr>`;

  return `<table class="w-full border-collapse text-xs"><thead><tr>${headers}</tr></thead><tbody>${rows}${empty}</tbody></table>`;
}

function getPreset(viewId: string) {
  return PIVOT_REMOTE_SAVED_VIEW_PRESETS.find((preset) => preset.viewId === viewId);
}

function getViewName(view: PivotSavedViewRecord) {
  return 'name' in view ? view.name ?? '(unnamed)' : '(deleted)';
}

function hasActiveView(views: readonly PivotSavedViewRecord[], viewId: string) {
  return views.some((view) => view.viewId === viewId && !('deleted' in view));
}

function chipClass(tone: string) {
  if (tone === 'warning') return 'border-amber-300 text-amber-700';
  if (tone === 'success') return 'border-emerald-300 text-emerald-700';
  return 'border-gray-300 text-gray-600';
}

function activityClass(tone: string) {
  if (tone === 'warning') return 'text-amber-700';
  if (tone === 'success') return 'text-emerald-700';
  return '';
}
Vue vue
<template>
  <div class="flex h-full min-h-[620px] gap-3">
    <aside class="w-[320px] shrink-0 space-y-3 overflow-auto border-r border-gray-200 pr-3 text-sm dark:border-gray-700">
      <h3 class="m-0 text-base font-semibold">Remote Pivot tools</h3>
      <p class="m-0 text-xs text-gray-500">{{ message }}</p>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Saved layouts</h4>
        <select :value="activeViewId" class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900" @change="selectView">
          <option v-for="preset in PIVOT_REMOTE_SAVED_VIEW_PRESETS" :key="preset.viewId" :value="preset.viewId">
            {{ preset.name }}
          </option>
          <option v-for="view in customViews" :key="view.viewId" :value="view.viewId">
            {{ getViewName(view) }}
          </option>
        </select>
        <p class="m-0 text-xs text-gray-500">{{ activePreset?.description ?? 'Saved field layout from the grid panel.' }}</p>
        <div class="flex flex-wrap gap-1">
          <button class="rounded border px-2 py-1" @click="saveView">Save current layout</button>
          <button class="rounded border px-2 py-1" @click="renameView">Rename</button>
          <button class="rounded border px-2 py-1" @click="duplicateView">Duplicate</button>
          <button class="rounded border px-2 py-1" @click="deleteView">Delete</button>
        </div>
        <div class="max-h-24 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
          <div v-for="view in activeViews" :key="view.viewId">
            {{ view.viewId }}: {{ getViewName(view) }}
          </div>
          <div v-if="!activeViews.length">Save a preset to create a user-owned view.</div>
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Drilldown</h4>
        <select v-model="activeDrilldownId" class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900">
          <option v-for="item in PIVOT_REMOTE_DRILLDOWN_CELLS" :key="item.id" :value="item.id">
            {{ item.label }}
          </option>
        </select>
        <div class="flex flex-wrap gap-1">
          <button class="rounded border px-2 py-1" @click="loadFacts">Load source rows</button>
          <button class="rounded border px-2 py-1" @click="resetDrilldown">Reset</button>
        </div>
        <div class="text-xs text-gray-600 dark:text-gray-300">
          {{ drilldownState.status }} - {{ drilldownState.rows.length }} of {{ drilldownState.totalCount }} rows - {{ drilldownState.columns.length }} columns
        </div>
        <div class="max-h-36 overflow-auto rounded border border-gray-200 dark:border-gray-700">
          <table class="w-full border-collapse text-xs">
            <thead>
              <tr>
                <th v-for="column in drilldownState.columns" :key="String(column.prop)" class="border-b px-2 py-1 text-left font-semibold dark:border-gray-700">
                  {{ column.name ?? column.prop }}
                </th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(row, rowIndex) in drilldownState.rows" :key="rowIndex">
                <td v-for="column in drilldownState.columns" :key="String(column.prop)" class="border-b px-2 py-1 dark:border-gray-800">
                  {{ row[column.prop] }}
                </td>
              </tr>
              <tr v-if="!drilldownState.rows.length">
                <td class="px-2 py-2 text-gray-500" :colspan="Math.max(drilldownState.columns.length, 1)">
                  No source rows loaded.
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Diagnostics</h4>
        <button class="rounded border px-2 py-1" @click="runDiagnostics">Run diagnostics</button>
        <div class="space-y-1 text-xs text-gray-600 dark:text-gray-300">
          <div v-for="row in diagnostics?.rows ?? []" :key="row.id">{{ row.label }}: {{ row.value }}</div>
          <div v-if="!diagnostics">No diagnostics yet</div>
        </div>
        <div class="flex flex-wrap gap-1">
          <span
            v-for="chip in diagnostics?.chips ?? []"
            :key="chip.id"
            class="rounded border px-2 py-0.5 text-xs"
            :class="chip.tone === 'warning' ? 'border-amber-300 text-amber-700' : chip.tone === 'success' ? 'border-emerald-300 text-emerald-700' : 'border-gray-300 text-gray-600'"
          >
            {{ chip.label }}
          </span>
        </div>
      </section>

      <section class="space-y-2">
        <h4 class="m-0 font-semibold">Remote activity</h4>
        <div class="max-h-28 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
          <div
            v-for="entry in activity"
            :key="entry.id"
            :class="entry.tone === 'warning' ? 'text-amber-700' : entry.tone === 'success' ? 'text-emerald-700' : ''"
          >
            {{ entry.label }}
          </div>
          <div v-if="!activity.length">No remote activity yet.</div>
        </div>
      </section>
    </aside>

    <div class="grid-shell h-full min-w-0 grow overflow-hidden">
      <RevoGrid
        class="h-full w-full cell-border"
        hide-attribution
        range
        resize
        readonly
        :colSize="180"
        :source="[]"
        :pivot.prop="pivot"
        :additionalData="additionalData"
        :theme="isDark ? 'darkCompact' : 'compact'"
        :plugins="plugins"
        :column-types="columnTypes"
        @pivot-config-update="configUpdate"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import RevoGrid, { type GridPlugin } from '@revolist/vue3-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  PivotDrilldownController,
  PivotPlugin,
  createPivotDiagnosticsModel,
  deletePivotSavedView,
  duplicatePivotSavedView,
  loadPivotView,
  renamePivotSavedView,
  savePivotView,
  type PivotConfig,
  type PivotDiagnosticsModel,
  type PivotDrilldownState,
  type PivotSavedViewRecord,
} from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import {
  PIVOT_REMOTE_DRILLDOWN_CELLS,
  PIVOT_REMOTE_DRILLDOWN_COLUMNS,
  PIVOT_REMOTE_FIELDS_VERSION,
  PIVOT_REMOTE_SAVED_VIEW_PRESETS,
  PIVOT_REMOTE_VIEW_ID,
  createPivotRemoteActivityHooks,
  createPivotRemoteDiagnosticsRequest,
  createPivotRemoteStore,
  serializePivotRemoteToolsLayout,
  type PivotRemoteToolsActivity,
  withPivotRemoteEngine,
  withPivotRemoteToolsUi,
} from '../sys-data/pivot.remote';

const USER_ID = 'portal-demo-user';
const { isDark } = currentThemeVue();
const activity = ref<PivotRemoteToolsActivity[]>([]);
const store = createPivotRemoteStore(createPivotRemoteActivityHooks(addActivity));
const drilldown = new PivotDrilldownController(store, { defaultLimit: 5 });

const diagnostics = ref<PivotDiagnosticsModel | null>(null);
const drilldownState = ref<PivotDrilldownState>(drilldown.getState());
const views = ref<PivotSavedViewRecord[]>([]);
const activeViewId = ref(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId);
const viewName = ref(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name);
const activeDrilldownId = ref(PIVOT_REMOTE_DRILLDOWN_CELLS[0].id);
const pivotConfig = ref(createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config));
const message = ref('Use the grid field panel to change dimensions, then save the layout.');

const columnTypes = ref({
  currency: new NumberColumnType('$0,0.00'),
});
const plugins: GridPlugin[] = [PivotPlugin, PaginationPlugin, RowOddPlugin];
const pivot = computed(() => pivotConfig.value);
const additionalData = computed(() => ({
  pagination: {
    itemsPerPage: 3,
    initialPage: 0,
    total: 0,
  },
}));
const activeViews = computed(() => views.value.filter((view) => !('deleted' in view)));
const customViews = computed(() => activeViews.value.filter((view) => !getPreset(view.viewId)));
const activePreset = computed(() => getPreset(activeViewId.value));
const activeDrilldown = computed(() => {
  return PIVOT_REMOTE_DRILLDOWN_CELLS.find((item) => item.id === activeDrilldownId.value) ?? PIVOT_REMOTE_DRILLDOWN_CELLS[0];
});

let unsubscribe: () => void = () => undefined;
onMounted(() => {
  unsubscribe = drilldown.subscribe((state) => {
    drilldownState.value = state;
  });
  void runDiagnostics();
});
onUnmounted(() => {
  unsubscribe();
  drilldown.abort();
});

async function runDiagnostics() {
  const response = await store.load(createPivotRemoteDiagnosticsRequest(getCurrentLayoutConfig()));
  diagnostics.value = createPivotDiagnosticsModel(response, {
    formatGeneratedAt: (value) => new Date(value).toLocaleTimeString(),
  });
  message.value = 'Diagnostics refreshed for the selected layout.';
}

async function loadFacts() {
  await drilldown.load({
    viewId: PIVOT_REMOTE_VIEW_ID,
    fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
    cell: activeDrilldown.value.cell,
    customColumns: PIVOT_REMOTE_DRILLDOWN_COLUMNS,
  });
  message.value = `Loaded source rows for ${activeDrilldown.value.label}.`;
}

function resetDrilldown() {
  drilldown.reset();
  message.value = 'Drilldown reset.';
}

async function saveView() {
  const preset = getPreset(activeViewId.value);
  const targetViewId = preset ? `custom-layout-${activeViews.value.length + 1}` : activeViewId.value;
  const targetName = preset ? `Saved Layout ${activeViews.value.length + 1}` : viewName.value;
  const saved = await savePivotView(store, {
    userId: USER_ID,
    viewId: targetViewId,
    name: targetName,
    config: getCurrentLayoutConfig(),
  });
  views.value = [...views.value.filter((view) => view.viewId !== saved.viewId), saved];
  activeViewId.value = saved.viewId;
  viewName.value = saved.name ?? saved.viewId;
  pivotConfig.value = createGridPivot(saved.config);
  message.value = `Saved ${saved.name ?? saved.viewId}.`;
}

async function selectView(event: Event) {
  const viewId = (event.target as HTMLSelectElement).value;
  activeViewId.value = viewId;
  const preset = getPreset(viewId);
  if (preset) {
    viewName.value = preset.name;
    pivotConfig.value = createGridPivot(preset.config);
    message.value = `Loaded ${preset.name} preset into the grid.`;
    return;
  }

  const saved = await loadPivotView(store, { userId: USER_ID, viewId });
  viewName.value = saved.name ?? saved.viewId;
  pivotConfig.value = createGridPivot(saved.config);
  message.value = `Loaded ${saved.name ?? saved.viewId} into the grid.`;
}

function renameView() {
  if (!hasActiveView(views.value, activeViewId.value)) {
    message.value = 'Save a view before renaming.';
    return;
  }
  const nextName = `${viewName.value} Renamed`;
  views.value = renamePivotSavedView(views.value, { userId: USER_ID, viewId: activeViewId.value, name: nextName });
  viewName.value = nextName;
  message.value = `Renamed ${activeViewId.value}.`;
}

function duplicateView() {
  if (!hasActiveView(views.value, activeViewId.value)) {
    message.value = 'Save a view before duplicating.';
    return;
  }
  const newViewId = `${activeViewId.value}-copy-${activeViews.value.length + 1}`;
  const nextName = `${viewName.value} Copy`;
  views.value = duplicatePivotSavedView(views.value, {
    userId: USER_ID,
    viewId: activeViewId.value,
    newViewId,
    name: nextName,
  });
  activeViewId.value = newViewId;
  viewName.value = nextName;
  const duplicate = activeViews.value.find((view) => view.viewId === newViewId);
  if (duplicate && 'config' in duplicate) {
    pivotConfig.value = createGridPivot(duplicate.config);
  }
  message.value = `Duplicated as ${newViewId}.`;
}

function deleteView() {
  if (!hasActiveView(views.value, activeViewId.value)) {
    message.value = 'Save a view before deleting.';
    return;
  }
  views.value = deletePivotSavedView(views.value, { userId: USER_ID, viewId: activeViewId.value });
  activeViewId.value = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId;
  viewName.value = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name;
  pivotConfig.value = createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config);
  message.value = 'Deleted active view.';
}

function configUpdate(event: CustomEvent<PivotConfig>) {
  pivotConfig.value = createGridPivot(event.detail ?? getCurrentLayoutConfig());
  message.value = 'Layout changed in the field panel. Save it to keep this arrangement.';
}

function getCurrentLayoutConfig(): Partial<PivotConfig> {
  return serializePivotRemoteToolsLayout(pivotConfig.value);
}

function createGridPivot(config: Partial<PivotConfig>) {
  return withPivotRemoteEngine(withPivotRemoteToolsUi(config), store);
}

function getPreset(viewId: string) {
  return PIVOT_REMOTE_SAVED_VIEW_PRESETS.find((preset) => preset.viewId === viewId);
}

function getViewName(view: PivotSavedViewRecord) {
  return 'name' in view ? view.name ?? '(unnamed)' : '(deleted)';
}

function hasActiveView(views: readonly PivotSavedViewRecord[], viewId: string) {
  return views.some((view) => view.viewId === viewId && !('deleted' in view));
}

function addActivity(entry: PivotRemoteToolsActivity) {
  activity.value = [entry, ...activity.value].slice(0, 8);
}
</script>
React tsx
import { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  PivotDrilldownController,
  PivotPlugin,
  createPivotDiagnosticsModel,
  deletePivotSavedView,
  duplicatePivotSavedView,
  loadPivotView,
  renamePivotSavedView,
  savePivotView,
  type PivotConfig,
  type PivotDiagnosticsModel,
  type PivotDrilldownState,
  type PivotSavedViewRecord,
} from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_REMOTE_DRILLDOWN_CELLS,
  PIVOT_REMOTE_DRILLDOWN_COLUMNS,
  PIVOT_REMOTE_FIELDS_VERSION,
  PIVOT_REMOTE_SAVED_VIEW_PRESETS,
  PIVOT_REMOTE_VIEW_ID,
  createPivotRemoteActivityHooks,
  createPivotRemoteDiagnosticsRequest,
  createPivotRemoteStore,
  serializePivotRemoteToolsLayout,
  type PivotRemoteToolsActivity,
  withPivotRemoteEngine,
  withPivotRemoteToolsUi,
} from '../sys-data/pivot.remote';

const USER_ID = 'portal-demo-user';

function PivotRemoteTools() {
  const { isDark } = currentTheme();
  const [activity, setActivity] = useState<PivotRemoteToolsActivity[]>([]);
  const store = useMemo(() => createPivotRemoteStore(createPivotRemoteActivityHooks((entry) => {
    setActivity((current) => [entry, ...current].slice(0, 8));
  })), []);
  const createGridPivot = (config: Partial<PivotConfig>) => withPivotRemoteEngine(withPivotRemoteToolsUi(config), store);
  const drilldown = useMemo(() => new PivotDrilldownController(store, { defaultLimit: 5 }), [store]);
  const [diagnostics, setDiagnostics] = useState<PivotDiagnosticsModel | null>(null);
  const [drilldownState, setDrilldownState] = useState<PivotDrilldownState>(drilldown.getState());
  const [views, setViews] = useState<PivotSavedViewRecord[]>([]);
  const [activeViewId, setActiveViewId] = useState(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId);
  const [viewName, setViewName] = useState(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name);
  const [activeDrilldownId, setActiveDrilldownId] = useState(PIVOT_REMOTE_DRILLDOWN_CELLS[0].id);
  const [pivotConfig, setPivotConfig] = useState(() => createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config));
  const [message, setMessage] = useState('Use the grid field panel to change dimensions, then save the layout.');
  const gridRef = useRef<HTMLRevoGridElement>(null);

  const columnTypes = useMemo(
    () => ({
      currency: new NumberColumnType('$0,0.00'),
    }),
    [],
  );
  const plugins = useMemo(() => [PivotPlugin, PaginationPlugin, RowOddPlugin] as any, []);
  const pivot = useMemo(() => pivotConfig, [pivotConfig]);
  const additionalData = useMemo(
    () => ({
      pagination: {
        itemsPerPage: 3,
        initialPage: 0,
        total: 0,
      },
    }),
    [],
  );

  const activeViews = views.filter((view) => !('deleted' in view));
  const customViews = activeViews.filter((view) => !getPreset(view.viewId));
  const activePreset = getPreset(activeViewId);
  const activeDrilldown = PIVOT_REMOTE_DRILLDOWN_CELLS.find((item) => item.id === activeDrilldownId)
    ?? PIVOT_REMOTE_DRILLDOWN_CELLS[0];

  useEffect(() => drilldown.subscribe(setDrilldownState), [drilldown]);
  useEffect(() => {
    const grid = gridRef.current;
    if (!grid) return;
    const handler = (event: Event) => {
      const detail = (event as CustomEvent<PivotConfig>).detail ?? getCurrentLayoutConfig();
      setPivotConfig(createGridPivot(detail));
      setMessage('Layout changed in the field panel. Save it to keep this arrangement.');
    };
    grid.addEventListener('pivot-config-update', handler);
    return () => grid.removeEventListener('pivot-config-update', handler);
  }, [store]);
  useEffect(() => {
    void runDiagnostics();
  }, []);

  async function runDiagnostics() {
    const response = await store.load(createPivotRemoteDiagnosticsRequest(getCurrentLayoutConfig()));
    setDiagnostics(createPivotDiagnosticsModel(response, {
      formatGeneratedAt: (value) => new Date(value).toLocaleTimeString(),
    }));
    setMessage('Diagnostics refreshed for the selected layout.');
  }

  async function loadFacts() {
    await drilldown.load({
      viewId: PIVOT_REMOTE_VIEW_ID,
      fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
      cell: activeDrilldown.cell,
      customColumns: PIVOT_REMOTE_DRILLDOWN_COLUMNS,
    });
    setMessage(`Loaded source rows for ${activeDrilldown.label}.`);
  }

  function resetDrilldown() {
    drilldown.reset();
    setMessage('Drilldown reset.');
  }

  async function saveView() {
    const preset = getPreset(activeViewId);
    const targetViewId = preset ? `custom-layout-${activeViews.length + 1}` : activeViewId;
    const targetName = preset ? `Saved Layout ${activeViews.length + 1}` : viewName;
    const saved = await savePivotView(store, {
      userId: USER_ID,
      viewId: targetViewId,
      name: targetName,
      config: getCurrentLayoutConfig(),
    });
    setViews((current) => [...current.filter((view) => view.viewId !== saved.viewId), saved]);
    setActiveViewId(saved.viewId);
    setViewName(saved.name ?? saved.viewId);
    setPivotConfig(createGridPivot(saved.config));
    setMessage(`Saved ${saved.name ?? saved.viewId}.`);
  }

  function renameView() {
    if (!hasActiveView(views, activeViewId)) {
      setMessage('Save a view before renaming.');
      return;
    }
    const nextName = `${viewName} Renamed`;
    setViews((current) => renamePivotSavedView(current, { userId: USER_ID, viewId: activeViewId, name: nextName }));
    setViewName(nextName);
    setMessage(`Renamed ${activeViewId}.`);
  }

  function duplicateView() {
    if (!hasActiveView(views, activeViewId)) {
      setMessage('Save a view before duplicating.');
      return;
    }
    const newViewId = `${activeViewId}-copy-${activeViews.length + 1}`;
    const nextName = `${viewName} Copy`;
    const nextViews = duplicatePivotSavedView(views, {
      userId: USER_ID,
      viewId: activeViewId,
      newViewId,
      name: nextName,
    });
    const duplicate = nextViews.find((view) => view.viewId === newViewId);
    setViews(nextViews);
    setActiveViewId(newViewId);
    setViewName(nextName);
    if (duplicate && 'config' in duplicate) {
      setPivotConfig(createGridPivot(duplicate.config));
    }
    setMessage(`Duplicated as ${newViewId}.`);
  }

  function deleteView() {
    if (!hasActiveView(views, activeViewId)) {
      setMessage('Save a view before deleting.');
      return;
    }
    setViews((current) => deletePivotSavedView(current, { userId: USER_ID, viewId: activeViewId }));
    setActiveViewId(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId);
    setViewName(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name);
    setPivotConfig(createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config));
    setMessage('Deleted active view.');
  }

  async function selectView(viewId: string) {
    setActiveViewId(viewId);
    const preset = getPreset(viewId);
    if (preset) {
      setViewName(preset.name);
      setPivotConfig(createGridPivot(preset.config));
      setMessage(`Loaded ${preset.name} preset into the grid.`);
      return;
    }

    const saved = await loadPivotView(store, { userId: USER_ID, viewId });
    setViewName(saved.name ?? saved.viewId);
    setPivotConfig(createGridPivot(saved.config));
    setMessage(`Loaded ${saved.name ?? saved.viewId} into the grid.`);
  }

  function getCurrentLayoutConfig(): Partial<PivotConfig> {
    return serializePivotRemoteToolsLayout(pivotConfig);
  }

  return (
    <div className="flex h-full min-h-[620px] gap-3">
      <aside className="w-[320px] shrink-0 space-y-3 overflow-auto border-r border-gray-200 pr-3 text-sm dark:border-gray-700">
        <h3 className="m-0 text-base font-semibold">Remote Pivot tools</h3>
        <p className="m-0 text-xs text-gray-500">{message}</p>

        <section className="space-y-2">
          <h4 className="m-0 font-semibold">Saved layouts</h4>
          <select
            value={activeViewId}
            className="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900"
            onChange={(event) => void selectView(event.currentTarget.value)}
          >
            {PIVOT_REMOTE_SAVED_VIEW_PRESETS.map((preset) => (
              <option key={preset.viewId} value={preset.viewId}>{preset.name}</option>
            ))}
            {customViews.map((view) => (
              <option key={view.viewId} value={view.viewId}>{getViewName(view)}</option>
            ))}
          </select>
          <p className="m-0 text-xs text-gray-500">{activePreset?.description ?? 'Saved field layout from the grid panel.'}</p>
          <div className="flex flex-wrap gap-1">
            <button className="rounded border px-2 py-1" onClick={() => void saveView()}>Save current layout</button>
            <button className="rounded border px-2 py-1" onClick={renameView}>Rename</button>
            <button className="rounded border px-2 py-1" onClick={duplicateView}>Duplicate</button>
            <button className="rounded border px-2 py-1" onClick={deleteView}>Delete</button>
          </div>
          <div className="max-h-24 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
            {activeViews.length ? activeViews.map((view) => (
              <div key={view.viewId}>{view.viewId}: {getViewName(view)}</div>
            )) : 'Save a preset to create a user-owned view.'}
          </div>
        </section>

        <section className="space-y-2">
          <h4 className="m-0 font-semibold">Drilldown</h4>
          <select
            value={activeDrilldownId}
            className="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900"
            onChange={(event) => setActiveDrilldownId(event.currentTarget.value)}
          >
            {PIVOT_REMOTE_DRILLDOWN_CELLS.map((item) => (
              <option key={item.id} value={item.id}>{item.label}</option>
            ))}
          </select>
          <div className="flex flex-wrap gap-1">
            <button className="rounded border px-2 py-1" onClick={() => void loadFacts()}>Load source rows</button>
            <button className="rounded border px-2 py-1" onClick={resetDrilldown}>Reset</button>
          </div>
          <div className="text-xs text-gray-600 dark:text-gray-300">
            {drilldownState.status} - {drilldownState.rows.length} of {drilldownState.totalCount} rows - {drilldownState.columns.length} columns
          </div>
          <div className="max-h-36 overflow-auto rounded border border-gray-200 dark:border-gray-700">
            <table className="w-full border-collapse text-xs">
              <thead>
                <tr>
                  {drilldownState.columns.map((column) => (
                    <th key={String(column.prop)} className="border-b px-2 py-1 text-left font-semibold dark:border-gray-700">
                      {column.name ?? column.prop}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {drilldownState.rows.map((row, rowIndex) => (
                  <tr key={rowIndex}>
                    {drilldownState.columns.map((column) => (
                      <td key={String(column.prop)} className="border-b px-2 py-1 dark:border-gray-800">
                        {String(row[column.prop] ?? '')}
                      </td>
                    ))}
                  </tr>
                ))}
                {!drilldownState.rows.length ? (
                  <tr>
                    <td className="px-2 py-2 text-gray-500" colSpan={Math.max(drilldownState.columns.length, 1)}>
                      No source rows loaded.
                    </td>
                  </tr>
                ) : null}
              </tbody>
            </table>
          </div>
        </section>

        <section className="space-y-2">
          <h4 className="m-0 font-semibold">Diagnostics</h4>
          <button className="rounded border px-2 py-1" onClick={() => void runDiagnostics()}>Run diagnostics</button>
          <div className="space-y-1 text-xs text-gray-600 dark:text-gray-300">
            {diagnostics?.rows.map((row) => <div key={row.id}>{row.label}: {row.value}</div>) ?? 'No diagnostics yet'}
          </div>
          <div className="flex flex-wrap gap-1">
            {diagnostics?.chips.map((chip) => (
              <span
                key={chip.id}
                className={`rounded border px-2 py-0.5 text-xs ${chip.tone === 'warning' ? 'border-amber-300 text-amber-700' : chip.tone === 'success' ? 'border-emerald-300 text-emerald-700' : 'border-gray-300 text-gray-600'}`}
              >
                {chip.label}
              </span>
            ))}
          </div>
        </section>

        <section className="space-y-2">
          <h4 className="m-0 font-semibold">Remote activity</h4>
          <div className="max-h-28 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
            {activity.length ? activity.map((entry) => (
              <div
                key={entry.id}
                className={entry.tone === 'warning' ? 'text-amber-700' : entry.tone === 'success' ? 'text-emerald-700' : ''}
              >
                {entry.label}
              </div>
            )) : 'No remote activity yet.'}
          </div>
        </section>
      </aside>
      <div className="grid-shell h-full min-w-0 grow overflow-hidden">
        <RevoGrid
          ref={gridRef}
          className="h-full w-full cell-border"
          hideAttribution
          range
          resize
          readonly
          colSize={180}
          source={[]}
          columns={[]}
          pivot={pivot}
          additionalData={additionalData as any}
          theme={isDark() ? 'darkCompact' : 'compact'}
          plugins={plugins}
          columnTypes={columnTypes}
        />
      </div>
    </div>
  );
}

function getPreset(viewId: string) {
  return PIVOT_REMOTE_SAVED_VIEW_PRESETS.find((preset) => preset.viewId === viewId);
}

function getViewName(view: PivotSavedViewRecord) {
  return 'name' in view ? view.name ?? '(unnamed)' : '(deleted)';
}

function hasActiveView(views: readonly PivotSavedViewRecord[], viewId: string) {
  return views.some((view) => view.viewId === viewId && !('deleted' in view));
}

export default PivotRemoteTools;
Angular ts
import { CommonModule } from '@angular/common';
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import type { OnDestroy, OnInit } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import type { ColumnProp, DataType } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  PivotDrilldownController,
  PivotPlugin,
  createPivotDiagnosticsModel,
  deletePivotSavedView,
  duplicatePivotSavedView,
  loadPivotView,
  renamePivotSavedView,
  savePivotView,
  type PivotConfig,
  type PivotDiagnosticsModel,
  type PivotDrilldownState,
  type PivotSavedViewRecord,
} from '@revolist/revogrid-enterprise';
import { PaginationPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  PIVOT_REMOTE_DRILLDOWN_CELLS,
  PIVOT_REMOTE_DRILLDOWN_COLUMNS,
  PIVOT_REMOTE_FIELDS_VERSION,
  PIVOT_REMOTE_SAVED_VIEW_PRESETS,
  PIVOT_REMOTE_VIEW_ID,
  createPivotRemoteActivityHooks,
  createPivotRemoteDiagnosticsRequest,
  createPivotRemoteStore,
  serializePivotRemoteToolsLayout,
  type PivotRemoteToolsActivity,
  withPivotRemoteEngine,
  withPivotRemoteToolsUi,
} from '../sys-data/pivot.remote';

const USER_ID = 'portal-demo-user';

@Component({
  selector: 'pivot-remote-tools-grid',
  standalone: true,
  imports: [CommonModule, RevoGrid],
  template: `
    <div class="flex h-full min-h-[620px] gap-3">
      <aside class="w-[320px] shrink-0 space-y-3 overflow-auto border-r border-gray-200 pr-3 text-sm dark:border-gray-700">
        <h3 class="m-0 text-base font-semibold">Remote Pivot tools</h3>
        <p class="m-0 text-xs text-gray-500">{{ message }}</p>

        <section class="space-y-2">
          <h4 class="m-0 font-semibold">Saved layouts</h4>
          <select class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900" [value]="activeViewId" (change)="selectView($event)">
            <option *ngFor="let preset of presets" [value]="preset.viewId">{{ preset.name }}</option>
            <option *ngFor="let view of customViews" [value]="view.viewId">{{ getViewName(view) }}</option>
          </select>
          <p class="m-0 text-xs text-gray-500">{{ activePreset?.description ?? 'Saved field layout from the grid panel.' }}</p>
          <div class="flex flex-wrap gap-1">
            <button class="rounded border px-2 py-1" (click)="saveView()">Save current layout</button>
            <button class="rounded border px-2 py-1" (click)="renameView()">Rename</button>
            <button class="rounded border px-2 py-1" (click)="duplicateView()">Duplicate</button>
            <button class="rounded border px-2 py-1" (click)="deleteView()">Delete</button>
          </div>
          <div class="max-h-24 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
            <div *ngFor="let view of activeViews">{{ view.viewId }}: {{ getViewName(view) }}</div>
            <div *ngIf="!activeViews.length">Save a preset to create a user-owned view.</div>
          </div>
        </section>

        <section class="space-y-2">
          <h4 class="m-0 font-semibold">Drilldown</h4>
          <select class="w-full rounded border px-2 py-1 text-xs dark:border-gray-700 dark:bg-gray-900" [value]="activeDrilldownId" (change)="selectDrilldown($event)">
            <option *ngFor="let item of drilldownCells" [value]="item.id">{{ item.label }}</option>
          </select>
          <div class="flex flex-wrap gap-1">
            <button class="rounded border px-2 py-1" (click)="loadFacts()">Load source rows</button>
            <button class="rounded border px-2 py-1" (click)="resetDrilldown()">Reset</button>
          </div>
          <div class="text-xs text-gray-600 dark:text-gray-300">
            {{ drilldownState.status }} - {{ drilldownState.rows.length }} of {{ drilldownState.totalCount }} rows - {{ drilldownState.columns.length }} columns
          </div>
          <div class="max-h-36 overflow-auto rounded border border-gray-200 dark:border-gray-700">
            <table class="w-full border-collapse text-xs">
              <thead>
                <tr>
                  <th *ngFor="let column of drilldownState.columns" class="border-b px-2 py-1 text-left font-semibold dark:border-gray-700">
                    {{ column.name ?? column.prop }}
                  </th>
                </tr>
              </thead>
              <tbody>
                <tr *ngFor="let row of drilldownState.rows">
                  <td *ngFor="let column of drilldownState.columns" class="border-b px-2 py-1 dark:border-gray-800">
                    {{ getCellValue(row, column.prop) }}
                  </td>
                </tr>
                <tr *ngIf="!drilldownState.rows.length">
                  <td class="px-2 py-2 text-gray-500" [attr.colspan]="Math.max(drilldownState.columns.length, 1)">
                    No source rows loaded.
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </section>

        <section class="space-y-2">
          <h4 class="m-0 font-semibold">Diagnostics</h4>
          <button class="rounded border px-2 py-1" (click)="runDiagnostics()">Run diagnostics</button>
          <div class="space-y-1 text-xs text-gray-600 dark:text-gray-300">
            <div *ngFor="let row of diagnostics?.rows ?? []">{{ row.label }}: {{ row.value }}</div>
            <div *ngIf="!diagnostics">No diagnostics yet</div>
          </div>
          <div class="flex flex-wrap gap-1">
            <span
              *ngFor="let chip of diagnostics?.chips ?? []"
              class="rounded border px-2 py-0.5 text-xs"
              [ngClass]="chipClass(chip.tone)"
            >
              {{ chip.label }}
            </span>
          </div>
        </section>

        <section class="space-y-2">
          <h4 class="m-0 font-semibold">Remote activity</h4>
          <div class="max-h-28 space-y-1 overflow-auto text-xs text-gray-600 dark:text-gray-300">
            <div *ngFor="let entry of activity" [ngClass]="activityClass(entry.tone)">{{ entry.label }}</div>
            <div *ngIf="!activity.length">No remote activity yet.</div>
          </div>
        </section>
      </aside>

      <div class="grid-shell h-full min-w-0 grow overflow-hidden">
        <revo-grid
          class="h-full w-full cell-border"
          [hideAttribution]="true"
          [range]="true"
          [resize]="true"
          [readonly]="true"
          [colSize]="180"
          [source]="[]"
          [pivot]="pivot"
          [pagination]="additionalData.pagination"
          [theme]="theme"
          [plugins]="plugins"
          [columnTypes]="columnTypes"
          (pivot-config-update)="configUpdate($event)"
        ></revo-grid>
      </div>
    </div>
  `,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  encapsulation: ViewEncapsulation.None,
})
export class PivotRemoteToolsGridComponent implements OnInit, OnDestroy {
  readonly Math = Math;
  readonly presets = PIVOT_REMOTE_SAVED_VIEW_PRESETS;
  readonly drilldownCells = PIVOT_REMOTE_DRILLDOWN_CELLS;
  activity: PivotRemoteToolsActivity[] = [];

  private store = createPivotRemoteStore(createPivotRemoteActivityHooks((entry) => {
    this.activity = [entry, ...this.activity].slice(0, 8);
  }));
  private drilldown = new PivotDrilldownController(this.store, { defaultLimit: 5 });
  private unsubscribe: () => void = () => undefined;
  private currentLayoutConfig: Partial<PivotConfig> = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config;

  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  diagnostics: PivotDiagnosticsModel | null = null;
  drilldownState: PivotDrilldownState = this.drilldown.getState();
  views: PivotSavedViewRecord[] = [];
  activeViewId = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId;
  viewName = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name;
  activeDrilldownId: string = PIVOT_REMOTE_DRILLDOWN_CELLS[0].id;
  message = 'Use the grid field panel to change dimensions, then save the layout.';

  columnTypes = {
    currency: new NumberColumnType('$0,0.00'),
  };
  plugins = [PivotPlugin, PaginationPlugin, RowOddPlugin];
  pivot = this.createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config);
  additionalData = {
    pagination: {
      itemsPerPage: 3,
      initialPage: 0,
      total: 0,
    },
  };

  get activeViews() {
    return this.views.filter((view) => !('deleted' in view));
  }

  get customViews() {
    return this.activeViews.filter((view) => !this.getPreset(view.viewId));
  }

  get activePreset() {
    return this.getPreset(this.activeViewId);
  }

  get activeDrilldown() {
    return this.drilldownCells.find((item) => item.id === this.activeDrilldownId) ?? this.drilldownCells[0];
  }

  ngOnInit() {
    this.unsubscribe = this.drilldown.subscribe((state) => {
      this.drilldownState = state;
    });
    void this.runDiagnostics();
  }

  ngOnDestroy() {
    this.unsubscribe();
    this.drilldown.abort();
  }

  async selectView(event: Event) {
    const viewId = (event.target as HTMLSelectElement).value;
    this.activeViewId = viewId;
    const preset = this.getPreset(viewId);
    if (preset) {
      this.viewName = preset.name;
      this.currentLayoutConfig = preset.config;
      this.pivot = this.createGridPivot(preset.config);
      this.message = `Loaded ${preset.name} preset into the grid.`;
      return;
    }

    const saved = await loadPivotView(this.store, { userId: USER_ID, viewId });
    this.viewName = saved.name ?? saved.viewId;
    this.currentLayoutConfig = saved.config;
    this.pivot = this.createGridPivot(saved.config);
    this.message = `Loaded ${saved.name ?? saved.viewId} into the grid.`;
  }

  selectDrilldown(event: Event) {
    this.activeDrilldownId = (event.target as HTMLSelectElement).value;
  }

  async runDiagnostics() {
    const response = await this.store.load(createPivotRemoteDiagnosticsRequest(this.getCurrentLayoutConfig()));
    this.diagnostics = createPivotDiagnosticsModel(response, {
      formatGeneratedAt: (value) => new Date(value).toLocaleTimeString(),
    });
    this.message = 'Diagnostics refreshed for the selected layout.';
  }

  async loadFacts() {
    await this.drilldown.load({
      viewId: PIVOT_REMOTE_VIEW_ID,
      fieldsVersion: PIVOT_REMOTE_FIELDS_VERSION,
      cell: this.activeDrilldown.cell,
      customColumns: PIVOT_REMOTE_DRILLDOWN_COLUMNS,
    });
    this.message = `Loaded source rows for ${this.activeDrilldown.label}.`;
  }

  resetDrilldown() {
    this.drilldown.reset();
    this.message = 'Drilldown reset.';
  }

  async saveView() {
    const preset = this.getPreset(this.activeViewId);
    const targetViewId = preset ? `custom-layout-${this.activeViews.length + 1}` : this.activeViewId;
    const targetName = preset ? `Saved Layout ${this.activeViews.length + 1}` : this.viewName;
    const saved = await savePivotView(this.store, {
      userId: USER_ID,
      viewId: targetViewId,
      name: targetName,
      config: this.getCurrentLayoutConfig(),
    });
    this.views = [...this.views.filter((view) => view.viewId !== saved.viewId), saved];
    this.activeViewId = saved.viewId;
    this.viewName = saved.name ?? saved.viewId;
    this.currentLayoutConfig = saved.config;
    this.pivot = this.createGridPivot(saved.config);
    this.message = `Saved ${saved.name ?? saved.viewId}.`;
  }

  renameView() {
    if (!hasActiveView(this.views, this.activeViewId)) {
      this.message = 'Save a view before renaming.';
      return;
    }
    const nextName = `${this.viewName} Renamed`;
    this.views = renamePivotSavedView(this.views, { userId: USER_ID, viewId: this.activeViewId, name: nextName });
    this.viewName = nextName;
    this.message = `Renamed ${this.activeViewId}.`;
  }

  duplicateView() {
    if (!hasActiveView(this.views, this.activeViewId)) {
      this.message = 'Save a view before duplicating.';
      return;
    }
    const newViewId = `${this.activeViewId}-copy-${this.activeViews.length + 1}`;
    const nextName = `${this.viewName} Copy`;
    this.views = duplicatePivotSavedView(this.views, {
      userId: USER_ID,
      viewId: this.activeViewId,
      newViewId,
      name: nextName,
    });
    this.activeViewId = newViewId;
    this.viewName = nextName;
    const duplicate = this.activeViews.find((view) => view.viewId === newViewId);
    if (duplicate && 'config' in duplicate) {
      this.currentLayoutConfig = duplicate.config;
      this.pivot = this.createGridPivot(duplicate.config);
    }
    this.message = `Duplicated as ${newViewId}.`;
  }

  deleteView() {
    if (!hasActiveView(this.views, this.activeViewId)) {
      this.message = 'Save a view before deleting.';
      return;
    }
    this.views = deletePivotSavedView(this.views, { userId: USER_ID, viewId: this.activeViewId });
    this.activeViewId = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].viewId;
    this.viewName = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].name;
    this.currentLayoutConfig = PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config;
    this.pivot = this.createGridPivot(PIVOT_REMOTE_SAVED_VIEW_PRESETS[0].config);
    this.message = 'Deleted active view.';
  }

  configUpdate(event: CustomEvent<PivotConfig>) {
    this.currentLayoutConfig = serializePivotRemoteToolsLayout(event.detail ?? this.currentLayoutConfig);
    this.pivot = this.createGridPivot(this.currentLayoutConfig);
    this.message = 'Layout changed in the field panel. Save it to keep this arrangement.';
  }

  getCurrentLayoutConfig(): Partial<PivotConfig> {
    return serializePivotRemoteToolsLayout(this.currentLayoutConfig);
  }

  private createGridPivot(config: Partial<PivotConfig>) {
    return withPivotRemoteEngine(withPivotRemoteToolsUi(config), this.store);
  }

  getPreset(viewId: string) {
    return PIVOT_REMOTE_SAVED_VIEW_PRESETS.find((preset) => preset.viewId === viewId);
  }

  getViewName(view: PivotSavedViewRecord) {
    return 'name' in view ? view.name ?? '(unnamed)' : '(deleted)';
  }

  getCellValue(row: DataType, prop: ColumnProp) {
    return row[prop] ?? '';
  }

  chipClass(tone: string) {
    if (tone === 'warning') return 'border-amber-300 text-amber-700';
    if (tone === 'success') return 'border-emerald-300 text-emerald-700';
    return 'border-gray-300 text-gray-600';
  }

  activityClass(tone: string) {
    if (tone === 'warning') return 'text-amber-700';
    if (tone === 'success') return 'text-emerald-700';
    return '';
  }
}

function hasActiveView(views: readonly PivotSavedViewRecord[], viewId: string) {
  return views.some((view) => view.viewId === viewId && !('deleted' in view));
}
Mock Remote Store ts
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',
    },
  });
}
  • Diagnostics model: converts remote response metadata into rows and chips your UI can render.
  • Drilldown controller: turns a summarized Pivot cell into table-ready source rows.
  • Field panel: lets users reorder and move row, column, value, and filter fields before saving a layout. The demo keeps unused fields in the Filters area so they can be moved into the active layout.
  • Saved-view helpers: serialize, load, rename, duplicate, and delete user-owned Pivot configs that can be written back to grid.pivot.
  • Remote store hooks: expose request activity such as load, drilldown, saveState, and loadState for support panels or audit trails.

Mental Model

The grid loads aggregated data through the remote store. Your surrounding app UI can reuse the same store to ask follow-up questions: “How fast was that request?”, “Which source rows produced this number?”, “Which saved layout should this user load?”, or “What remote operation just ran?”.

All helpers below are exported from the Enterprise Pivot package.

import {
PivotDrilldownController,
createPivotDiagnosticsModel,
deletePivotSavedView,
duplicatePivotSavedView,
loadPivotView,
renamePivotSavedView,
savePivotView,
type PivotConfig,
type PivotLoadRequest,
type PivotRemoteStoreHooks,
type PivotSavedViewRecord,
} from '@revolist/revogrid-enterprise';

Remote Pivot responses can include meta data such as elapsed time, cache status, generated timestamp, and warnings. createPivotDiagnosticsModel converts that raw response into a simple display model.

Use it when you want a side panel, footer, debug drawer, or support view that explains what the last remote request did.

const response = await remoteStore.load({
requestId: `pivot-diagnostics-${Date.now()}`,
viewId: 'sales',
fieldsVersion: 'sha256:fields-v1',
loadOptions: {
rows: [{ selector: 'region' }],
columns: [{ selector: 'quarter' }],
totalSummary: [{ selector: 'sales', summaryType: 'sum' }],
groupSummary: [{ selector: 'sales', summaryType: 'sum' }],
},
viewport: {
rowAxis: { offset: 0, limit: 50 },
columnAxis: { offset: 0, limit: 12 },
},
uiState: {
collapsedByDefault: true,
rowHeaderLayout: 'tree',
},
} satisfies PivotLoadRequest);
const diagnostics = createPivotDiagnosticsModel(response, {
formatGeneratedAt: (value) => new Date(value).toLocaleString(),
});
for (const row of diagnostics.rows) {
console.log(row.label, row.value);
}
for (const chip of diagnostics.chips) {
console.log(chip.label, chip.tone);
}

The returned model has:

  • rows: stable rows such as elapsed time, generated time, visible rows, and visible columns.
  • chips: compact status items such as cache status or warnings.
  • warnings: warning strings from the response metadata.
  • hasWarnings: a boolean for showing warning states in your UI.

The helper does not make network requests. It only formats a PivotLoadResponse that your remote store already returned. In the live demo, the diagnostics panel refreshes against the currently selected layout so the visible rows and columns reflect that saved view.

Aggregated Pivot cells often need a “show source rows” action. PivotDrilldownController wraps the remote drilldown contract and keeps table state for you.

The controller owns:

  • current status: idle, loading, success, or error
  • table columns inferred from returned rows or customColumns
  • source rows for the selected summary cell
  • total matching row count
  • request cancellation when a newer drilldown request starts
const drilldown = new PivotDrilldownController(remoteStore, {
defaultLimit: 100,
});
const unsubscribe = drilldown.subscribe((state) => {
renderDrilldownTable({
loading: state.loading,
columns: state.columns,
rows: state.rows,
totalCount: state.totalCount,
error: state.error,
});
});
await drilldown.load({
viewId: 'sales',
fieldsVersion: 'sha256:fields-v1',
cell: {
rowPath: ['North', 'Jane'],
columnPath: [2024, 'Q1'],
},
customColumns: ['region', 'rep', 'year', 'quarter', 'sales'],
offset: 0,
limit: 25,
});
const state = drilldown.getState();
console.log(state.status, state.rows.length, state.totalCount);
unsubscribe();
drilldown.abort();

The cell value should identify the analytical cell, not the visible DOM cell. Use Pivot paths like rowPath and columnPath so your backend can apply the same grouping context and return matching fact rows. The controller state is table-ready: render state.columns, state.rows, state.totalCount, and state.status directly in your surrounding UI.

Saved views are user-owned Pivot configurations. They are useful when users build several layouts, such as “Revenue by Region” and “Quarterly Margin”.

The helpers use store.saveState() and store.loadState() for persistence. Rename, duplicate, and delete operate on your app’s list of saved-view records, so you can combine them with your own sync logic, menus, permissions, or undo UI. In an interactive screen, listen for pivot-config-update from the field panel and save that latest config. Loading a view should apply the returned config back to the direct Pivot binding (grid.pivot, pivot, or framework equivalent) so the report changes immediately.

const USER_ID = 'user-123';
let views: PivotSavedViewRecord[] = [];
const config: Partial<PivotConfig> = {
dimensions: [
{ prop: 'region' },
{ prop: 'rep' },
{ prop: 'quarter' },
{ prop: 'sales' },
],
rows: ['region', 'rep'],
columns: ['quarter'],
values: [{ prop: 'sales', aggregator: 'sum' }],
totals: { grandTotal: true, subtotals: true },
};
const saved = await savePivotView(remoteStore, {
userId: USER_ID,
viewId: 'north-sales',
name: 'North Sales',
config,
});
views = [...views.filter((view) => view.viewId !== saved.viewId), saved];
const loaded = await loadPivotView(remoteStore, {
userId: USER_ID,
viewId: 'north-sales',
});
grid.pivot = {
...loaded.config,
engine: {
mode: 'server',
remoteStore,
viewId: 'sales',
fieldsVersion: 'sha256:fields-v1',
},
};
views = renamePivotSavedView(views, {
userId: USER_ID,
viewId: 'north-sales',
name: 'North Sales by Quarter',
});
views = duplicatePivotSavedView(views, {
userId: USER_ID,
viewId: 'north-sales',
newViewId: 'north-sales-copy',
name: 'North Sales Copy',
});
views = deletePivotSavedView(views, {
userId: USER_ID,
viewId: 'north-sales',
});

savePivotView stores a JSON-serializable Pivot config. It validates common Pivot fields before sending state to the store, which helps catch accidental functions, class instances, or invalid axis values before they become a saved view.

HttpPivotRemoteStore accepts lifecycle hooks for telemetry. They are useful for request logs, support drawers, cache badges, and lightweight audit trails near the grid.

const hooks: PivotRemoteStoreHooks = {
requestStarted: ({ type }) => {
activity.unshift(`${type}: started`);
},
requestSucceeded: ({ type, cacheStatus }) => {
activity.unshift(`${type}: succeeded${cacheStatus ? ` (${cacheStatus})` : ''}`);
},
requestFailed: ({ type }) => {
activity.unshift(`${type}: failed`);
},
cacheStatusChanged: ({ cacheStatus }) => {
activity.unshift(`cache: ${cacheStatus}`);
},
};

The live example uses those hooks with the same mocked HttpPivotRemoteStore that powers the grid. Running diagnostics, loading source rows, saving a view, and loading a view all update the activity log.

These helpers assume the remote store supports the operation you call:

  • Diagnostics needs load() responses with useful meta, rowAxis, and columnAxis data.
  • Drilldown needs an adapter or store with a drilldown() method.
  • Saved views need saveState() and loadState().
  • Activity logs need store hooks or equivalent instrumentation around your request layer.

You can implement those methods against any backend: REST, GraphQL, local storage for a demo, or an internal state service. RevoGrid does not require a specific saved-view backend.