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 (memoryag-grid-enterprise-license.md). Покрывает: sales.myveda.ru, base.myveda.ru. Internal staff only — Deployment License не куплена. Регистрация: app/lib/agGridSetup.ts —AllEnterpriseModule+LicenseManager.setLicenseKey.
TL;DR
- Row Grouping > Tree Data в 99% наших случаев. Tree Data использовать только если иерархия динамическая И Pivot не нужен.
- Группировка должна быть управляемой пользователем — не зашитой в columnDefs. Включать
rowGroupPanelShow: 'always'+enableRowGroup: trueна каждой перетаскиваемой колонке + полный SideBar с drop zones. - Master/Detail без
isRowMasterc серверным флагом — баг. Любой раскрываемый узел БЕЗ данных рендерит «No Rows To Show» — это анти-паттерн (см. реальный косяк163942d). - Charts/Sparklines в v33+ — отдельный пакет
ag-charts-enterprise. У нас стоитag-charts-community— часть опций может молча отвалиться. - Theming v33+ — новый API. У нас работает legacy через
theme="legacy"prop на компоненте + старые CSS-импорты. Это сознательный выбор, не сломано. - Модули в v33+ раздроблены. Регистрировать
AllEnterpriseModuleцеликом (текущий путь) — ОК. Тонкая регистрация — для tree-shaking, у нас не приоритет. - Стандарт 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. RangeSelectionModule → CellSelectionModule (внутри 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-функциональность:
npm install ag-charts-enterprise- В setup.ts добавить:
import { AgChartsEnterpriseModule } from 'ag-charts-enterprise'; import { IntegratedChartsModule } from 'ag-grid-enterprise'; ModuleRegistry.registerModules([ AllEnterpriseModule, IntegratedChartsModule.with(AgChartsEnterpriseModule), ]); - Лицензия
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"serverSideDatasourcecallback который дёргает API сrequest.startRow / endRow / sortModel / filterModel / rowGroupCols / pivotCols / valueCols.- Backend должен уметь group-by + agg + pivot SQL по этим параметрам (наш текущий backend этого не умеет — это R&D-задача).
Сейчас не приоритет. В Сводке/Воронке/Базе — десятки/сотни строк, CSRM достаточно. Перейти на SSRM имеет смысл когда metrika_visit пойдёт в Воронку как сырой источник (миллионы строк).
Анти-паттерны (не делать)
- Tree Data + Pivot одновременно — несовместимо. Pivot не работает.
aggFunc: 'avg'на процентной колонке — даёт среднее процентов, не корректный пересчёт. Использовать custom aggFunc или valueGetter с aggData (Pattern 3).- Не указать
aggFuncна numeric колонке — group rows будут пустые. Минимум'sum'для всех чисел. - Несколько pinned-left колонок для иерархии — это анти-паттерн ручного indent. Использовать native
autoGroupColumnDef. groupDefaultExpanded={-1}для CEO-дашборда — всё развёрнуто, информационный шум.cellRendererбезvalueGetterпри экспорте Excel — Excel не использует cell renderers, цифры будут сырые.- Inline edit без
editable: true— double-click не сработает. masterDetailбезisRowMaster— пустые detail-grid с «No Rows To Show» (наш реальный косяк, см. таблицу выше).- Sparkline на 10k+ rows без виртуализации — тормозит. AG Grid виртуализирует автоматически если
suppressColumnVirtualisationНЕ включён. enableRangeSelection={true}(устарело в v33+) — заменить наcellSelection.- Зашитая иерархия через
rowGroup: trueбезenableRowGroupи безrowGroupPanelShow— пользователь не может менять группировку. Канон — управляемая через panel. - CSS arbitrary хаки
[&>div...]:hiddenдля скрытия колонок — использоватьhide: trueв colDef илиgridApi.setColumnVisible(). <AgGridReact>напрямую в табах — обходит shared<AnalyticsGrid>, разваливает консистентность.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 таблицы
- Открыл этот SKILL целиком.
- Принял Pattern 0 решение: Row Grouping или Tree Data — обоснование в PR description.
- Все перетаскиваемые колонки имеют
enableRowGroup: true/enableValue: true/enablePivot: trueсоответственно. rowGroupPanelShow: 'always'(или'onlyWhenGrouping').suppressGroupChangesColumnVisibility+suppressDragLeaveHidesColumnsвключены.- Если есть Master/Detail — есть
isRowMasterс серверным флагом. - Все процентные колонки используют custom aggFunc или valueGetter с aggData (НЕ
aggFunc: 'avg'). - Excel Export тестирован: цифры отформатированы, group headers сохранены.
- Локализация русская (
localeText). - Используется
<AnalyticsGrid>, не прямой<AgGridReact>. - Тестировал в браузере: разворачивание, drag в Row Groups, Pivot Mode toggle, Excel export.
- Скриншот в 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 проверить руками:
RowGroupingModule→RowGroupingModule + RowGroupingPanelModule + TreeDataModule + PivotModule + GroupFilterModuleесли регистрация была адресной (еслиAllEnterpriseModule— без правок).MenuModule→ColumnMenuModule + ContextMenuModule.GridChartsModule→IntegratedChartsModule.with(AgChartsEnterpriseModule)(требует отдельный пакетag-charts-enterprise).
С v33 на v35 — никаких breaking changes по сравнению с v33, только баг-фиксы и новые фичи.
Reference docs (читать перед нестандартной фичей)
- Modules: https://www.ag-grid.com/react-data-grid/modules/
- Row Grouping: https://www.ag-grid.com/react-data-grid/grouping/
- Row Group Panel: https://www.ag-grid.com/react-data-grid/grouping-group-panel/
- Tree Data: https://www.ag-grid.com/react-data-grid/tree-data/ (только для динамической иерархии)
- Pivoting: https://www.ag-grid.com/react-data-grid/pivoting/
- Aggregation: https://www.ag-grid.com/react-data-grid/aggregation/
- Custom Aggregation: https://www.ag-grid.com/react-data-grid/aggregation-custom-functions/
- Master/Detail: https://www.ag-grid.com/react-data-grid/master-detail/
- Master/Detail Master Rows: https://www.ag-grid.com/react-data-grid/master-detail-master-rows/
- SideBar / Tool Panel: https://www.ag-grid.com/react-data-grid/side-bar/
- Status Bar: https://www.ag-grid.com/react-data-grid/status-bar/
- Cell Selection: https://www.ag-grid.com/react-data-grid/cell-selection/
- Sparklines: https://www.ag-grid.com/react-data-grid/sparklines-overview/
- Integrated Charts: https://www.ag-grid.com/react-data-grid/integrated-charts/
- Excel Export: https://www.ag-grid.com/react-data-grid/excel-export/
- Theming v33+: https://www.ag-grid.com/react-data-grid/theming/
- Localisation: https://www.ag-grid.com/react-data-grid/localisation/
- Server-Side Row Model: https://www.ag-grid.com/react-data-grid/server-side-model/
- v31 → v33 Migration: https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-33/
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про inlinemarginTop:32, не Tailwind). - При апгрейде на новую мажорную версию AG Grid: запустить
npx @ag-grid-devtools/cli@latest migrate, прогнать сборку, тестировать Сводку/Воронку/Базу руками, обновить этот SKILL.