aggrid-enterprise

star 0

AG Grid Enterprise v35 React паттерны для аналитических дашбордов VEDA. Когда Row Grouping vs Tree Data, как настраивать Row Group Panel + SideBar drop zones, Master/Detail с динамическим isRowMaster, Pivot Mode, custom aggFuncs для weighted ratios, Status Bar + Cell Selection, Sparklines (нюанс ag-charts-community vs enterprise), Excel Export, Theming v33+. Документ покрывает реальные косяки в git и как их не повторить.

VedaAstro By VedaAstro schedule Updated 5/25/2026

name: aggrid-enterprise description: AG Grid Enterprise v35 React паттерны для аналитических дашбордов VEDA. Когда Row Grouping vs Tree Data, как настраивать Row Group Panel + SideBar drop zones, Master/Detail с динамическим isRowMaster, Pivot Mode, custom aggFuncs для weighted ratios, Status Bar + Cell Selection, Sparklines (нюанс ag-charts-community vs enterprise), Excel Export, Theming v33+. Документ покрывает реальные косяки в git и как их не повторить.

AG Grid Enterprise v35 — для VEDA Analytics

Версия в проде: ag-grid-community ^35.1.0, ag-grid-enterprise ^35.2.1, ag-grid-react ^35.1.0, ag-charts-community ^13.1.0. Лицензия: Multiple Applications, 1 dev, до 06.05.2027 (memory ag-grid-enterprise-license.md). Покрывает: sales.myveda.ru, base.myveda.ru. Internal staff only — Deployment License не куплена. Регистрация: app/lib/agGridSetup.tsAllEnterpriseModule + LicenseManager.setLicenseKey.


TL;DR

  1. Row Grouping > Tree Data в 99% наших случаев. Tree Data использовать только если иерархия динамическая И Pivot не нужен.
  2. Группировка должна быть управляемой пользователем — не зашитой в columnDefs. Включать rowGroupPanelShow: 'always' + enableRowGroup: true на каждой перетаскиваемой колонке + полный SideBar с drop zones.
  3. Master/Detail без isRowMaster c серверным флагом — баг. Любой раскрываемый узел БЕЗ данных рендерит «No Rows To Show» — это анти-паттерн (см. реальный косяк 163942d).
  4. Charts/Sparklines в v33+ — отдельный пакет ag-charts-enterprise. У нас стоит ag-charts-community — часть опций может молча отвалиться.
  5. Theming v33+ — новый API. У нас работает legacy через theme="legacy" prop на компоненте + старые CSS-импорты. Это сознательный выбор, не сломано.
  6. Модули в v33+ раздроблены. Регистрировать AllEnterpriseModule целиком (текущий путь) — ОК. Тонкая регистрация — для tree-shaking, у нас не приоритет.
  7. Стандарт VEDA: всё через shared <AnalyticsGrid>. Прямой <AgGridReact> в табах запрещён.

Что мы реально ломали в проде (учиться на своих, а не повторять)

Дата Коммит Косяк Корневая причина Что делать впредь
05.05.2026 163942d feat(funnel): per-row диагностики из API Master/Detail рендерит пустую таблицу «No Rows To Show» при разворачивании любого лендинга кроме Я.Директа isRowMaster отсутствует — раскрытие включено для всех строк, а endpoint возвращает данные только для платных Pattern 5: isRowMaster обязательно с серверным флагом has_detail_data
05.05.2026 c012a67 refactor: Tree Data → Row Grouping Архитектурный ход правильный, но иерархия по-прежнему зашита в columnDefs (rowGroup: true, rowGroupIndex), без rowGroupPanelShow и без enableRowGroup Скопировали структуру с Tree Data 1:1, не задействовали UX-обвязку Row Grouping Pattern 2: панель + enableRowGroup на колонках + пресеты
28-30.04.2026 1cb882e feat(svodka): Tree Data + master/detail Выбрали Tree Data для Сводки → автоматически отрезали Pivot Mode (несовместим) Не прочитали reference table «Row Grouping vs Tree Data» Решение Pattern 0 (RG vs TD) принимать ДО кода
04-05.05.2026 вся серия database-panel 17 плоских колонок без column groups, «Платформа» как столбец а не dimension для группировки Не задействовали column groups + rowGroup для платформы Pattern 2 для База: rowGroup по платформе + 4 семантических column groups
05-06.05.2026 2fa833b feat(voronka): sparkline Возможный риск падения расширенных sparkline-опций Установлен ag-charts-community, а нужен ag-charts-enterprise для full feature set Pattern 9: либо upgrade пакета, либо basic-опции

Все косяки сводятся к одному: скилл читали поверхностно, тех-доку AG Grid не открывали. Перед любой правкой AG Grid-таблицы — открыть этот файл целиком. Перед нестандартной фичей — открыть AG Grid docs и прислать ссылку в PR.


Pattern 0: Row Grouping vs Tree Data (решение ДО кода)

Параметр Row Grouping Tree Data
Иерархия задаётся через rowGroup: true на колонках ИЛИ через UI panel getDataPath callback
Pivot Mode работает? да нет
Pivot drop zone в SideBar да нет
Глубина иерархии фиксированная (по rowGroup-колонкам) произвольная динамическая
Best для аналитика по dimensions с возможным разворотом период→столбцы файловые системы, орг-структуры, дерево комментариев

ВЫВОД для VEDA: 99% — Row Grouping. Tree Data только если:

  • глубина дерева заранее неизвестна (рекурсия),
  • И Pivot Mode точно не понадобится.

Если сомневаешься — Row Grouping. Если позже окажется что нужен динамический tree, мигрируй в Tree Data адресно.


Pattern 1: Setup (License + Modules + Theme)

app/lib/agGridSetup.ts — единая точка регистрации, импортируется в каждом компоненте с AG Grid.

import { AllEnterpriseModule, LicenseManager, ModuleRegistry } from 'ag-grid-enterprise';
import type { ColDef } from 'ag-grid-community';

ModuleRegistry.registerModules([AllEnterpriseModule]);

const LICENSE_KEY =
  process.env.NEXT_PUBLIC_AG_GRID_LICENSE_KEY ||
  process.env.AG_GRID_LICENSE_KEY ||
  '';

if (LICENSE_KEY) {
  LicenseManager.setLicenseKey(LICENSE_KEY);
} else if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'test') {
  console.warn('[ag-grid] LICENSE_KEY not set — Enterprise watermark будет показан');
}

export const AG_THEME = 'ag-theme-quartz';

export const defaultColDef: ColDef = {
  resizable: true,
  sortable: true,
  filter: true,
  enableRowGroup: true,
  enableValue: true,
  enablePivot: true,
};

Важно:

  • Тема в setup и в shared <AnalyticsGrid> должна быть одна и та же. На 06.05.2026 в setup стояло 'ag-theme-alpine', в <AnalyticsGrid>'ag-theme-quartz'. Расхождение = баг. Канон — quartz.
  • defaultColDef в setup и в <AnalyticsGrid> — слиты в одно место (либо setup, либо wrapper, не оба).
  • AllEnterpriseModule включает: ClientSideRowModelModule, ServerSideRowModelModule, RowGroupingModule, RowGroupingPanelModule, TreeDataModule, PivotModule, GroupFilterModule, MasterDetailModule, ColumnMenuModule, ContextMenuModule, ClipboardModule, ExcelExportModule, CsvExportModule, CellSelectionModule, StatusBarModule, SideBarModule, ColumnsToolPanelModule, FiltersToolPanelModule, IntegratedChartsModule, SparklinesModule, ValidationModule. Если позже понадобится tree-shaking — мигрировать на адресные модули.

CSS импорты (legacy theming, текущий путь):

// в app/components/analytics/analytics-grid.tsx:
import '@/app/lib/agGridSetup';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';

Сам <AgGridReact> получает theme="legacy" prop — это говорит гриду использовать CSS-классы вместо нового Theming API. Не путать с легаси-режимом устаревшей темы.


Pattern 2: Row Grouping с управляемой панелью (главный паттерн для всех табов)

import { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';

const columnDefs: ColDef[] = [
  { field: 'funnel', headerName: 'Воронка', enableRowGroup: true, rowGroup: true, rowGroupIndex: 0, hide: true },
  { field: 'source', headerName: 'Источник', enableRowGroup: true, rowGroup: true, rowGroupIndex: 1, hide: true },
  { field: 'landing', headerName: 'Лендинг', enableRowGroup: true, rowGroup: true, rowGroupIndex: 2, hide: true },

  { field: 'visits', headerName: 'Визиты', aggFunc: 'sum', enableValue: true, valueFormatter: numFmt },
  { field: 'clicks', headerName: 'Клики', aggFunc: 'sum', enableValue: true, valueFormatter: numFmt },
  { field: 'revenue', headerName: 'Выручка', aggFunc: 'sum', enableValue: true, valueFormatter: moneyFmt },
];

const autoGroupColumnDef: ColDef = {
  headerName: 'Воронка / Источник / Лендинг',
  minWidth: 350,
  pinned: 'left',
  cellRendererParams: { suppressCount: false },
  sortable: true,
};

<AgGridReact
  rowData={rows}
  columnDefs={columnDefs}
  autoGroupColumnDef={autoGroupColumnDef}
  groupDisplayType="singleColumn"
  groupDefaultExpanded={1}
  rowGroupPanelShow="always"
  suppressGroupChangesColumnVisibility
  suppressDragLeaveHidesColumns
  // SideBar — см. Pattern 6
/>

Обязательные флаги (без них пользователь видит баги):

  • enableRowGroup: true на КАЖДОЙ колонке, которую пользователь сможет тащить в Row Group panel. Без этого drag не работает.
  • enableValue: true на числовых колонках — иначе их нельзя в Values drop zone.
  • enablePivot: true если планируется Pivot Mode (например period_label).
  • rowGroupPanelShow: 'always' — chip-bar сверху как в AG Grid Performance demo. Альтернативы: 'onlyWhenGrouping' (показать только когда есть группы), 'never' (скрыть полностью).
  • suppressGroupChangesColumnVisibility: true — без него колонка исчезает при drag в Row Groups, пользователь думает что сломал.
  • suppressDragLeaveHidesColumns: true — без него колонка прячется при drag-out из header, тоже сюрприз.

groupDisplayType — выбор:

  • 'singleColumn' (рекомендую): одна Group column, дерево разворачивается через arrow. Канон CEO-дашборда.
  • 'multipleColumns': каждый уровень в своей колонке (Power BI Matrix).
  • 'groupRows': группы как полноширинные строки (компакт).
  • Не использовать 'custom' без сильной причины.

groupDefaultExpanded:

  • 0 — всё свёрнуто (CEO открыл — видит только верхний уровень = метрика).
  • 1 — Result/верхний уровень развёрнут, остальные свёрнуты. Лучше всего для Сводки.
  • -1 — всё развёрнуто (информационный шум, не использовать).

Пресеты группировки (UI кнопки):

const applyPreset = (preset: 'by-funnel' | 'by-source' | 'by-landing' | 'flat') => {
  const states: Record<string, ColumnState[]> = {
    'by-funnel': [{ colId: 'funnel', rowGroup: true, rowGroupIndex: 0, hide: true }],
    'by-source': [{ colId: 'source', rowGroup: true, rowGroupIndex: 0, hide: true }],
    'by-landing': [
      { colId: 'source', rowGroup: true, rowGroupIndex: 0, hide: true },
      { colId: 'landing', rowGroup: true, rowGroupIndex: 1, hide: true },
    ],
    'flat': [],
  };
  gridApi.applyColumnState({ state: states[preset], applyOrder: true });
};

Pattern 3: Custom aggFunc для weighted ratios (CR, ROMI, CTR, Open rate)

aggFunc: 'avg' на процентной колонке даёт среднее процентов, не корректный пересчёт от сумм. На parent-row это всегда ошибка.

Правильный путь A — через registered aggFunc (работает с column-state save и UI panel):

const aggFuncs = useMemo(() => ({
  weightedCR: (params: IAggFuncParams) => {
    const num = params.values.reduce((s, v: any) => s + (v?._num ?? 0), 0);
    const den = params.values.reduce((s, v: any) => s + (v?._den ?? 0), 0);
    return den > 0 ? { _num: num, _den: den, value: num / den * 100, toString: () => (num / den * 100).toFixed(1) + '%' } : null;
  },
}), []);

const columnDefs: ColDef[] = [
  { field: 'visits', aggFunc: 'sum', enableValue: true },
  { field: 'leads',  aggFunc: 'sum', enableValue: true },
  {
    headerName: 'CR Визит→Лид',
    valueGetter: (p) => p.data ? { _num: p.data.leads, _den: p.data.visits, value: p.data.visits > 0 ? p.data.leads / p.data.visits * 100 : null } : null,
    aggFunc: 'weightedCR',
    valueFormatter: (p) => p.value?.value != null ? `${p.value.value.toFixed(1)}%` : '—',
  },
];

<AgGridReact aggFuncs={aggFuncs} ... />

Правильный путь Б — через valueGetter с aggData (проще, но не работает при column-state save):

{
  headerName: 'CR Визит→Лид',
  valueGetter: (p) => {
    const data = p.node?.group ? p.node.aggData : p.data;
    const v = data?.visits ?? 0;
    const l = data?.leads ?? 0;
    return v > 0 ? l / v * 100 : null;
  },
  valueFormatter: (p) => p.value != null ? `${p.value.toFixed(1)}%` : '—',
}

Используй путь А когда нужен Pivot Mode и сохранение column state (drag в Values panel). Путь Б — для простых случаев когда CR никогда не будет в Values drop zone.


Pattern 4: Pivot Mode (период в столбцы)

const columnDefs: ColDef[] = [
  { field: 'funnel', enableRowGroup: true, rowGroup: true, rowGroupIndex: 0, hide: true },
  { field: 'source', enableRowGroup: true, rowGroupIndex: 1, hide: true },
  { field: 'period_label', enablePivot: true, pivot: true },
  { field: 'visits', aggFunc: 'sum', enableValue: true },
  { field: 'revenue', aggFunc: 'sum', enableValue: true },
];

const [pivotMode, setPivotMode] = useState(false);

<>
  <button onClick={() => setPivotMode(p => !p)}>{pivotMode ? 'Выкл Pivot' : 'Pivot Mode'}</button>
  <AgGridReact
    pivotMode={pivotMode}
    pivotPanelShow="onlyWhenPivoting"
    sideBar={...}
    ...
  />
</>

При pivotMode=true колонки period_label (2026-01, 2026-02, ...) автоматически становятся столбцами, под каждым visits и revenue агрегаты.

Несовместимости:

  • Pivot Mode + Tree Data = не работает.
  • Pivot Mode + Master/Detail = технически работает, но UX странный, не рекомендуется.
  • Pivot Mode + Server-Side Row Model = требует server pivot support, у нас не реализовано.

Pattern 5: Master/Detail с динамическим isRowMaster (главный фикс для Воронки)

Анти-паттерн (см. косяк коммита 163942d):

<AgGridReact masterDetail={true} detailCellRendererParams={detailParams} />

Раскрытие включено для ВСЕХ строк → endpoint возвращает пусто для большинства → пользователь видит «No Rows To Show».

Правильно — серверный флаг:

API возвращает row с полем has_detail_data: boolean (на стороне backend пред-проверка через EXISTS или count).

const isRowMaster = useCallback((dataItem: FunnelRow | undefined) => {
  return Boolean(dataItem?.has_detail_data);
}, []);

const detailCellRendererParams = useMemo(() => ({
  detailGridOptions: {
    columnDefs: [
      { field: 'campaign_id', headerName: 'ID' },
      { field: 'campaign_name', headerName: 'Кампания' },
      { field: 'spend', valueFormatter: moneyFmt },
      { field: 'romi_pct', valueFormatter: pctFmt },
    ],
    defaultColDef: { resizable: true, sortable: true },
  },
  getDetailRowData: async (params: any) => {
    const res = await authFetch(
      `/api/analytics/funnel-paths/${encodeURIComponent(params.data.landing)}/campaigns?from=${df}&to=${dt}`
    );
    const json = await res.json();
    params.successCallback(json.rows ?? []);
  },
}), [authFetch, df, dt]);

<AgGridReact
  masterDetail
  isRowMaster={isRowMaster}
  detailCellRendererParams={detailCellRendererParams}
  detailRowAutoHeight
  keepDetailRows
  keepDetailRowsCount={10}
/>

Правила:

  • isRowMaster обязательно. Без него — баг.
  • Флаг has_detail_data приходит с сервера, не вычисляется на клиенте по эвристике (легко ошибиться).
  • При applyTransaction({ update }) AG Grid пересчитывает isRowMaster — можно динамически снимать раскрытие.
  • Master/Detail работает только с Client-Side или Server-Side Row Models на master grid.
  • Detail grid может быть любого row model (включая отдельный server-side).

Pattern 6: SideBar — полный config с drop zones

const sideBar = useMemo(() => ({
  toolPanels: [
    {
      id: 'columns',
      labelDefault: 'Колонки',
      iconKey: 'columns',
      toolPanel: 'agColumnsToolPanel',
      toolPanelParams: {
        suppressRowGroups: false,
        suppressValues: false,
        suppressPivots: false,
        suppressPivotMode: false,
        suppressColumnFilter: false,
        suppressColumnSelectAll: false,
        suppressColumnExpandAll: false,
      },
    },
    {
      id: 'filters',
      labelDefault: 'Фильтры',
      iconKey: 'filter',
      toolPanel: 'agFiltersToolPanel',
    },
  ],
  defaultToolPanel: undefined,
  hiddenByDefault: false,
  position: 'right' as const,
}), []);

Локализация (для VEDA):

const localeText = {
  // SideBar tool panels
  columns: 'Колонки',
  filters: 'Фильтры',
  // Drop zones в Columns tool panel
  rowGroupColumnsEmptyMessage: 'Перетащи колонку для группировки',
  valueColumnsEmptyMessage: 'Перетащи колонку для агрегации',
  pivotColumnsEmptyMessage: 'Перетащи колонку для столбцов pivot',
  pivotMode: 'Pivot Mode',
  groups: 'Группы',
  values: 'Значения',
  pivots: 'Столбцы pivot',
  // Empty/loading
  noRowsToShow: 'Нет данных за период',
  loadingOoo: 'Загрузка...',
  // Status bar
  totalAndFilteredRows: 'Строк',
  totalRows: 'Всего',
  filteredRows: 'Отфильтровано',
  selectedRows: 'Выделено',
  // Aggregation
  sum: 'Σ',
  avg: 'Ср.',
  min: 'Мин',
  max: 'Макс',
  count: 'Кол-во',
};

Полный список ключей: https://www.ag-grid.com/react-data-grid/localisation/


Pattern 7: Status Bar + Cell Selection

const statusBar = useMemo(() => ({
  statusPanels: [
    { statusPanel: 'agTotalAndFilteredRowCountComponent', align: 'left' as const },
    { statusPanel: 'agSelectedRowCountComponent', align: 'center' as const },
    {
      statusPanel: 'agAggregationComponent',
      align: 'right' as const,
      statusPanelParams: { aggFuncs: ['count', 'sum', 'avg', 'min', 'max'] },
    },
  ],
}), []);

<AgGridReact
  statusBar={statusBar}
  cellSelection                  // v33+ имя (бывший enableRangeSelection)
  ...
/>

agAggregationComponent показывает Σ/avg/min/max/count выделенного диапазона ячеек — Excel-like UX. Без cellSelection агрегация не появится.

Миграция с v31: enableRangeSelection устарел, в v33+ это cellSelection. RangeSelectionModuleCellSelectionModule (внутри AllEnterpriseModule всё ещё работает).


Pattern 8: Inline Edit (Сводка — план)

const columnDefs: ColDef[] = [
  {
    field: 'plan_value',
    headerName: 'План',
    editable: (p) => !p.node?.group,            // только на leaf, не на group rows
    cellEditor: 'agNumberCellEditor',
    cellEditorParams: { min: 0, precision: 0 },
    aggFunc: 'sum',
    valueFormatter: moneyFmt,
  },
  { field: 'fact_value', headerName: 'Факт', aggFunc: 'sum', valueFormatter: moneyFmt },
];

const onCellValueChanged = useCallback(async (event: CellValueChangedEvent) => {
  if (event.colDef.field !== 'plan_value') return;
  if (event.newValue === event.oldValue) return;

  try {
    const res = await authFetch('/api/analytics/plan', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        metric_key: event.data.metric_key,
        period_start: event.data.period_start,
        plan_value: event.newValue,
      }),
    });
    if (!res.ok) {
      event.api.undoCellEdits();
      toast.error('Не удалось сохранить план');
    }
  } catch {
    event.api.undoCellEdits();
    toast.error('Сеть недоступна');
  }
}, [authFetch]);

<AgGridReact onCellValueChanged={onCellValueChanged} ... />

Правила:

  • editable: true обязательно — без него double-click не открывает редактор.
  • Group-row не должна быть редактируемой ((p) => !p.node?.group).
  • undoCellEdits() при ошибке сохранения — иначе UI разойдётся с сервером.
  • getRowId обязательно если данные обновляются через React Query / SWR — иначе AG Grid пересоздаст строки и потеряет фокус.

Pattern 9: Sparklines — нюанс ag-charts

ВАЖНО: В v33+ Charts вынесены в отдельный пакет ag-charts-enterprise (или ag-charts-community для базовых типов). ag-grid-enterprise НЕ включает их автоматически.

Что у нас в проде (06.05.2026):

  • Установлен ag-charts-community ^13.1.0 (базовый).
  • НЕ установлен ag-charts-enterprise.
  • Sparklines работают, но Enterprise-опции (advanced tooltip, range selection on sparkline, custom shapes, multi-series) могут молча отвалиться.

Минимальный пример (работает с community):

const columnDefs: ColDef[] = [
  {
    field: 'revenue_trend',
    headerName: 'Тренд',
    cellRenderer: 'agSparklineCellRenderer',
    cellRendererParams: {
      sparklineOptions: {
        type: 'area',
        stroke: '#7F56D9',
        fill: 'rgba(127, 86, 217, 0.15)',
        marker: { enabled: false },
        axis: { type: 'category' },
      },
    },
    minWidth: 120,
    sortable: false,
  },
];

Если нужна полная Enterprise-функциональность:

  1. npm install ag-charts-enterprise
  2. В setup.ts добавить:
    import { AgChartsEnterpriseModule } from 'ag-charts-enterprise';
    import { IntegratedChartsModule } from 'ag-grid-enterprise';
    ModuleRegistry.registerModules([
      AllEnterpriseModule,
      IntegratedChartsModule.with(AgChartsEnterpriseModule),
    ]);
    
  3. Лицензия ag-charts-enterprise отдельная от ag-grid-enterprise — уточнить покрывает ли наш ключ оба продукта.

Если Алекс хочет Integrated Charts (выделить диапазон → Insert Chart): нужно enableCharts={true} на гриде + IntegratedChartsModule.with(AgChartsEnterpriseModule). Без enterprise charts — кнопка показана, но при клике fallback на community-чарт ограниченных типов.


Pattern 10: Excel Export

const handleExport = useCallback(() => {
  gridRef.current?.api?.exportDataAsExcel({
    fileName: `voronka-${df}-${dt}.xlsx`,
    sheetName: 'Воронка',
    columnGroups: true,
    allColumns: false,
    onlySelected: false,
    processCellCallback: (params) => {
      if (typeof params.value === 'number' && params.column.getColDef().valueFormatter) {
        return params.formatValue(params.value);
      }
      return params.value;
    },
  });
}, [df, dt]);

Тонкости:

  • cellRenderer НЕ применяется при экспорте (Excel читает только raw value/valueGetter/valueFormatter). Чтобы цифры в Excel были отформатированы как в UI — processCellCallback обязателен.
  • columnGroups: true сохраняет column group headers (merged cells в Excel).
  • allColumns: false — экспорт только видимых колонок (рекомендую). true — включая скрытые.
  • Pivot-режим: добавить skipPinnedTop: false, чтобы pinned-bottom (ИТОГО) попадал в файл.

Pattern 11: Theming — legacy vs v33+ new API

В v33+ Theming API сменился на программный.

Новый путь:

import { themeQuartz } from 'ag-grid-community';
const theme = themeQuartz.withParams({
  accentColor: '#7F56D9',
  headerBackgroundColor: '#F9FAFB',
  fontFamily: 'Inter, sans-serif',
});
<AgGridReact theme={theme} ... />
// CSS импорты НЕ нужны

Legacy путь (наш текущий, осознанный выбор):

import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';
<div className="ag-theme-quartz" style={{height: 600}}>
  <AgGridReact theme="legacy" ... />
</div>

Почему legacy:

  • Меньше переписывать (миграция с v31 к новому API — много работы).
  • Кастомизация через CSS-переменные --ag-foreground-color и т.д. — привычно дизайнерам.
  • Глобальный mode переключения: provideGlobalGridOptions({ theme: 'legacy' }) — тогда не нужно ставить theme="legacy" на каждый компонент.

Когда мигрировать на новый API: если станет проблемой держать CSS-импорты, или нужна тонкая программная кастомизация per-grid (разные темы на разных табах). Сейчас не приоритет.


Pattern 12: Server-Side Row Model (когда переключаться)

Client-Side Row Model (CSRM) держит весь dataset в памяти. Лимит — 100k rows, дальше деградация при сортировке/фильтре/группировке.

Server-Side Row Model (SSRM) лениво подгружает блоки с сервера, аггрегации/группировки делает на сервере.

Когда переключаться на SSRM:

  • 50k строк после фильтра.

  • 100k строк в датасете.

  • Pivot по миллионам строк (тогда server-side pivot обязателен).
  • Live-data (WebSocket updates).

Что меняется в коде:

  • rowModelType="serverSide"
  • serverSideDatasource callback который дёргает API с request.startRow / endRow / sortModel / filterModel / rowGroupCols / pivotCols / valueCols.
  • Backend должен уметь group-by + agg + pivot SQL по этим параметрам (наш текущий backend этого не умеет — это R&D-задача).

Сейчас не приоритет. В Сводке/Воронке/Базе — десятки/сотни строк, CSRM достаточно. Перейти на SSRM имеет смысл когда metrika_visit пойдёт в Воронку как сырой источник (миллионы строк).


Анти-паттерны (не делать)

  1. Tree Data + Pivot одновременно — несовместимо. Pivot не работает.
  2. aggFunc: 'avg' на процентной колонке — даёт среднее процентов, не корректный пересчёт. Использовать custom aggFunc или valueGetter с aggData (Pattern 3).
  3. Не указать aggFunc на numeric колонке — group rows будут пустые. Минимум 'sum' для всех чисел.
  4. Несколько pinned-left колонок для иерархии — это анти-паттерн ручного indent. Использовать native autoGroupColumnDef.
  5. groupDefaultExpanded={-1} для CEO-дашборда — всё развёрнуто, информационный шум.
  6. cellRenderer без valueGetter при экспорте Excel — Excel не использует cell renderers, цифры будут сырые.
  7. Inline edit без editable: true — double-click не сработает.
  8. masterDetail без isRowMaster — пустые detail-grid с «No Rows To Show» (наш реальный косяк, см. таблицу выше).
  9. Sparkline на 10k+ rows без виртуализации — тормозит. AG Grid виртуализирует автоматически если suppressColumnVirtualisation НЕ включён.
  10. enableRangeSelection={true} (устарело в v33+) — заменить на cellSelection.
  11. Зашитая иерархия через rowGroup: true без enableRowGroup и без rowGroupPanelShow — пользователь не может менять группировку. Канон — управляемая через panel.
  12. CSS arbitrary хаки [&>div...]:hidden для скрытия колонок — использовать hide: true в colDef или gridApi.setColumnVisible().
  13. <AgGridReact> напрямую в табах — обходит shared <AnalyticsGrid>, разваливает консистентность.
  14. cellStyle: {display:'flex'} + cellRenderer без flex:1 на рендерере — см. ловушку ниже.

⚠️ Ловушка: cellStyle display:flex + cellRenderer (AG Grid v35)

Симптом: кастомный cellRenderer с justify-between — иконка/каретка висит в центре ячейки вместо правого края.

Причина: AG Grid v35 с cellStyle: {display:'flex'} на колонке рендерит span рендерера прямо в .ag-cell (flex-контейнер), без промежуточного .ag-cell-value. Span как flex-item имеет flex: 0 1 auto → схлопывается до content-width (может быть 13px при пустом тексте).

Диагностика:

// В browser console/Playwright:
const cell = document.querySelector('.ag-cell[col-id="myCol"]');
const span = cell.firstElementChild;
console.log(getComputedStyle(span).flex, getComputedStyle(span).width);
// Если flex="0 1 auto" и width≈13px — это ловушка

Фикс — inline style flex:1 на рендерере:

// ❌ Не работает — Tailwind w-full в flex-item контексте AG Grid
<span className="flex w-full justify-between">...</span>

// ✅ Правильно — inline style flex:1 надёжнее Tailwind в AG Grid
<span style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 4, minWidth: 0 }}>
  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{text}</span>
  <Icon className="size-3 shrink-0" />
</span>

Доп. ловушка — пустая строка из valueFormatter:
valueFormatted ?? fallback не ловит '' (пустая строка). Используй:

const text = (valueFormatted != null && valueFormatted !== '')
  ? valueFormatted
  : ((value == null || value === '') ? '—' : String(value));

Performance

  • CSRM до 100k rows OK при group + agg. Выше — SSRM.
  • Pivot с >100 unique pivot values → slow rendering. Ограничь period range.
  • suppressColumnVirtualisation — НЕ выключать (включён по умолчанию). Только для small datasets ≤500 rows и только если нужны pinned columns с горизонтальным скроллом.
  • getRowId обязательно для частых обновлений данных через React Query/SWR — иначе AG Grid пересоздаёт строки и теряет focus/expansion state.
  • rowBuffer={10} — дефолт ОК. Увеличивать только если визуальные тиры при scroll.
  • animateRows={false} для больших датасетов — анимация съедает FPS.

VEDA Standard: shared <AnalyticsGrid> wrapper (ОБЯЗАТЕЛЬНО)

Принцип: один shared компонент в app/components/analytics/analytics-grid.tsx. Все табы аналитики (Сводка/Воронка/База/Команда/Диагностики/Финансы) используют ТОЛЬКО его. Прямой <AgGridReact> в табах запрещён.

API контракт

'use client';
import { useMemo, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import type { ColDef, GridOptions, CellValueChangedEvent } from 'ag-grid-community';
import '@/app/lib/agGridSetup';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-quartz.css';
import { Button } from '@/components/base/buttons/button';
import { Download01 } from '@untitledui/icons';

export interface AnalyticsGridProps<T = any> {
  rowData: T[];
  columnDefs: ColDef[];
  groupHeaderName?: string;
  pinnedBottomRowData?: T[];
  onCellValueChanged?: (event: CellValueChangedEvent) => void;
  exportFileName?: string;
  groupDefaultExpanded?: number;
  height?: number | string;
  masterDetailParams?: GridOptions['detailCellRendererParams'];
  isRowMaster?: GridOptions['isRowMaster'];
  loading?: boolean;
  rowGroupPanelShow?: 'always' | 'onlyWhenGrouping' | 'never';
  pivotMode?: boolean;
  aggFuncs?: GridOptions['aggFuncs'];
  getRowId?: GridOptions['getRowId'];
}

export function AnalyticsGrid<T = any>(props: AnalyticsGridProps<T>) {
  const gridRef = useRef<AgGridReact<T>>(null);

  const autoGroupColumnDef = useMemo<ColDef>(() => ({
    headerName: props.groupHeaderName ?? 'Группа',
    minWidth: 350,
    pinned: 'left',
    cellRendererParams: { suppressCount: false },
    sortable: true,
  }), [props.groupHeaderName]);

  const defaultColDef = useMemo<ColDef>(() => ({
    sortable: true,
    resizable: true,
    filter: true,
    enableValue: true,
    enableRowGroup: true,
    enablePivot: true,
  }), []);

  const sideBar = useMemo(() => ({
    toolPanels: [
      {
        id: 'columns',
        labelDefault: 'Колонки',
        iconKey: 'columns',
        toolPanel: 'agColumnsToolPanel',
        toolPanelParams: {
          suppressRowGroups: false,
          suppressValues: false,
          suppressPivots: false,
          suppressPivotMode: false,
        },
      },
      {
        id: 'filters',
        labelDefault: 'Фильтры',
        iconKey: 'filter',
        toolPanel: 'agFiltersToolPanel',
      },
    ],
    position: 'right' as const,
    hiddenByDefault: false,
  }), []);

  const statusBar = useMemo(() => ({
    statusPanels: [
      { statusPanel: 'agTotalAndFilteredRowCountComponent', align: 'left' as const },
      { statusPanel: 'agSelectedRowCountComponent', align: 'center' as const },
      {
        statusPanel: 'agAggregationComponent',
        align: 'right' as const,
        statusPanelParams: { aggFuncs: ['count', 'sum', 'avg', 'min', 'max'] },
      },
    ],
  }), []);

  const handleExport = useCallback(() => {
    if (!props.exportFileName) return;
    gridRef.current?.api?.exportDataAsExcel({
      fileName: `${props.exportFileName}.xlsx`,
      sheetName: props.groupHeaderName ?? 'Данные',
      columnGroups: true,
      processCellCallback: (params) => {
        if (typeof params.value === 'number' && params.column.getColDef().valueFormatter) {
          return params.formatValue(params.value);
        }
        return params.value;
      },
    });
  }, [props.exportFileName, props.groupHeaderName]);

  const computedHeight = props.height ?? Math.min(Math.max(280, (props.rowData?.length ?? 0) * 40 + 200), 720);

  return (
    <div className="flex flex-col gap-3" style={{ marginTop: 16 }}>
      {props.exportFileName && (
        <div className="flex justify-end">
          <Button size="sm" color="secondary" iconLeading={Download01} onClick={handleExport}>
            Экспорт в Excel
          </Button>
        </div>
      )}
      <div className="ag-theme-quartz w-full" style={{ height: computedHeight, marginTop: 32 }}>
        <AgGridReact<T>
          ref={gridRef}
          theme="legacy"
          rowData={props.rowData}
          columnDefs={props.columnDefs}
          autoGroupColumnDef={autoGroupColumnDef}
          defaultColDef={defaultColDef}
          groupDisplayType="singleColumn"
          groupDefaultExpanded={props.groupDefaultExpanded ?? 0}
          rowGroupPanelShow={props.rowGroupPanelShow ?? 'always'}
          suppressGroupChangesColumnVisibility
          suppressDragLeaveHidesColumns
          suppressAggFuncInHeader
          pinnedBottomRowData={props.pinnedBottomRowData}
          sideBar={sideBar}
          statusBar={statusBar}
          cellSelection
          enableCharts
          rowHeight={40}
          headerHeight={36}
          groupHeaderHeight={28}
          tooltipShowDelay={200}
          suppressCellFocus={!props.onCellValueChanged}
          onCellValueChanged={props.onCellValueChanged}
          masterDetail={!!props.masterDetailParams}
          detailCellRendererParams={props.masterDetailParams}
          isRowMaster={props.isRowMaster}
          detailRowAutoHeight
          keepDetailRows
          pivotMode={props.pivotMode ?? false}
          pivotPanelShow="onlyWhenPivoting"
          aggFuncs={props.aggFuncs}
          getRowId={props.getRowId}
          loading={props.loading}
          localeText={{
            noRowsToShow: 'Нет данных за период',
            loadingOoo: 'Загрузка...',
            columns: 'Колонки',
            filters: 'Фильтры',
            rowGroupColumnsEmptyMessage: 'Перетащи колонку для группировки',
            valueColumnsEmptyMessage: 'Перетащи колонку для агрегации',
            pivotColumnsEmptyMessage: 'Перетащи для столбцов pivot',
            pivotMode: 'Pivot Mode',
            groups: 'Группы',
            values: 'Значения',
            pivots: 'Столбцы pivot',
          }}
        />
      </div>
    </div>
  );
}

Использование на табах (примеры)

Воронка:

<AnalyticsGrid
  rowData={data?.rows ?? []}
  columnDefs={voronkaColDefs}
  groupHeaderName="Воронка / Источник / Лендинг"
  exportFileName={`voronka-${df}-${dt}`}
  isRowMaster={(d) => Boolean(d?.has_campaigns)}
  masterDetailParams={campaignsDetailParams}
/>

Сводка (с inline plan-edit):

<AnalyticsGrid
  rowData={data?.rows ?? []}
  columnDefs={svodkaColDefs}
  groupHeaderName="Уровень / Группа / Метрика"
  exportFileName={`svodka-${df}-${dt}`}
  groupDefaultExpanded={1}
  onCellValueChanged={handlePlanEdit}
/>

База (с rowGroup по платформе):

const baseColDefs: ColDef[] = [
  { field: 'source', headerName: 'Платформа', enableRowGroup: true, rowGroup: true, rowGroupIndex: 0, hide: true },
  { field: 'asset', headerName: 'Канал' },
  { field: 'sent', aggFunc: 'sum', enableValue: true, valueFormatter: numFmt, /* group: 'Активность рассылок' */ },
  // ...
];

<AnalyticsGrid
  rowData={data?.rows ?? []}
  columnDefs={baseColDefs}
  groupHeaderName="Платформа / Канал"
  exportFileName={`baza-${df}-${dt}`}
/>

Запреты

  • Прямой <AgGridReact> в табах. Только через <AnalyticsGrid>.
  • Пере-настройка sideBar / statusBar / theme / groupDisplayType на уровне таба.
  • Вложенные таблицы. Drill-down — только через masterDetailParams.
  • Графики/карточки внутри grid-блока. Это Tableau-стиль, отдельная страница.
  • Tab отвечает только за rowData, columnDefs, filter row, master/detail callback. Всё остальное в shared.

Чек-лист перед PR с правкой AG Grid таблицы

  1. Открыл этот SKILL целиком.
  2. Принял Pattern 0 решение: Row Grouping или Tree Data — обоснование в PR description.
  3. Все перетаскиваемые колонки имеют enableRowGroup: true / enableValue: true / enablePivot: true соответственно.
  4. rowGroupPanelShow: 'always' (или 'onlyWhenGrouping').
  5. suppressGroupChangesColumnVisibility + suppressDragLeaveHidesColumns включены.
  6. Если есть Master/Detail — есть isRowMaster с серверным флагом.
  7. Все процентные колонки используют custom aggFunc или valueGetter с aggData (НЕ aggFunc: 'avg').
  8. Excel Export тестирован: цифры отформатированы, group headers сохранены.
  9. Локализация русская (localeText).
  10. Используется <AnalyticsGrid>, не прямой <AgGridReact>.
  11. Тестировал в браузере: разворачивание, drag в Row Groups, Pivot Mode toggle, Excel export.
  12. Скриншот в PR.

Migration v31 → v35 (если найдёшь старый код)

npx @ag-grid-devtools/cli@latest migrate --from=31 --to=33.0

Codemod автоматически:

  • Меняет @ag-grid-community/* импорты на ag-grid-community.
  • Заменяет enableRangeSelection на cellSelection.
  • Подставляет theme: 'legacy' или provideGlobalGridOptions({ theme: 'legacy' }).
  • Обновляет регистрацию модулей.

После codemod проверить руками:

  • RowGroupingModuleRowGroupingModule + RowGroupingPanelModule + TreeDataModule + PivotModule + GroupFilterModule если регистрация была адресной (если AllEnterpriseModule — без правок).
  • MenuModuleColumnMenuModule + ContextMenuModule.
  • GridChartsModuleIntegratedChartsModule.with(AgChartsEnterpriseModule) (требует отдельный пакет ag-charts-enterprise).

С v33 на v35 — никаких breaking changes по сравнению с v33, только баг-фиксы и новые фичи.


Reference docs (читать перед нестандартной фичей)


VEDA-specific facts

  • License key memo: ~/.claude/projects/-Users-alex-Projects/memory/ag-grid-enterprise-license.md.
  • Setup file: app/lib/agGridSetup.ts.
  • Shared wrapper: app/components/analytics/analytics-grid.tsx.
  • Канон-тема: ag-theme-quartz + theme="legacy" prop.
  • Filter row над гридом: UUI ButtonGroup + DateRangePicker (см. memory feedback_ui_spacing_inline_style_for_ag_grid.md про inline marginTop:32, не Tailwind).
  • При апгрейде на новую мажорную версию AG Grid: запустить npx @ag-grid-devtools/cli@latest migrate, прогнать сборку, тестировать Сводку/Воронку/Базу руками, обновить этот SKILL.
Install via CLI
npx skills add https://github.com/VedaAstro/veda-skills --skill aggrid-enterprise
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator