This guide explains how to embed the Report Engine designer/preview into your own React
application. The bundled apps/app-pc is a complete, runnable reference of everything here.
React 18+, TypeScript, and Ant Design 6 are required. The component library is built for these and ships ESM only.
The frontend is a pnpm monorepo with three publishable packages plus a demo app:
| Package | Role |
|---|---|
@coding-report/report-univer |
Univer spreadsheet React wrapper (snapshot import/export, fonts) |
@coding-report/report-api |
Backend API client (axios instance + auto-unwrapping of SingleResponse/MultiResponse) |
@coding-report/report-engine |
Report designer + data-source/data-model management components (pure UI, never calls APIs itself) |
Build/dependency order is report-univer → report-api → report-engine. The key design rule:
report-engine components are pure UI — they receive data via props and call back via
injected service objects; they never import report-api directly. You wire the two together in
your app (see §4).
The packages are workspace packages in this repo (workspace:*). In your own project, depend on
the three published packages and their peers:
report-univer depends on the @univerjs/* 0.25 packages (core, sheets, sheets-ui, presets,
engine-formula, etc.) — install the same set the demo app uses if your bundler does not hoist
them. See apps/app-pc/package.json for the exact list.
report-api ships a preconfigured axios instance with baseURL: '/api' and a response
interceptor that unwraps the SingleResponse/MultiResponse envelope automatically. You only
need to make /api reach the backend.
Dev server proxy (Rsbuild example, rsbuild.config.ts):
export default defineConfig({
server: {
proxy: {
'/api': { target: 'http://127.0.0.1:8090', changeOrigin: true },
},
},
});In production, serve the frontend behind the same origin as the backend, or front both with a
gateway that routes /api to the engine service.
Every management component takes a service (or *Service) object whose methods you implement
by delegating to report-api functions. This keeps the library API-agnostic. Example — the
data-source management page is literally this:
import { DataSourceManager } from '@coding-report/report-engine';
import type { DataSourceService } from '@coding-report/report-engine';
import {
listDataSources, getDataSource, saveDataSource, deleteDataSource,
introspectDatasets, introspectByConfig, introspectSql,
uploadDataFile, testConnection, listDataSourceTypes,
} from '@coding-report/report-api';
const service: DataSourceService = {
list: (current, pageSize) => listDataSources(current, pageSize),
get: (id) => getDataSource(id),
save: (dto) => saveDataSource(dto),
remove: (id) => deleteDataSource(id),
introspect: (id, tableNames) => introspectDatasets(id, tableNames),
introspectByConfig: (dto, tableNames) => introspectByConfig(dto, tableNames),
introspectSql: (id, sql) => introspectSql(id, sql),
uploadDataFile: (file, type) => uploadDataFile(file, type),
testConnection: (dto) => testConnection(dto),
listDriverTypes: async () => (await listDataSourceTypes(1, 100)).list,
};
export default function DataSourcesPage() {
return <DataSourceManager service={service} />;
}The data-model management page follows the same pattern with DataModelListPage +
DataModelService / DataModelDesignerService (see apps/app-pc/src/pages/datamodels.tsx).
ReportEngine is the three-pane designer (data model / spreadsheet / properties). Feed it the
data model pieces as props and inject a renderService to enable the built-in
preview/export/drill flow:
import { ReportEngine } from '@coding-report/report-engine';
import type { ReportEngineHandle } from '@coding-report/report-engine';
import {
loadReportConfig, saveReportConfig,
previewReport, renderReport, drillReport,
importExcel, fetchFonts, fetchFunctions,
} from '@coding-report/report-api';
const engineRef = useRef<ReportEngineHandle>(null);
// load a report by id, then populate datasets/relationships/transforms from
// config.dataModel (the backend enriches GET /configs/{id} with a model view)
// engineRef.current?.loadReportConfig(config)
return (
<ReportEngine
datasets={datasets} // Dataset[] from the loaded data model
relationships={relationships} // Relationship[]
transforms={transforms} // TransformItem[] (data-transform dictionaries)
dataModelId={dataModelId}
functions={functions} // ExpressionCatalog from fetchFunctions()
engineRef={engineRef}
renderService={{ preview: previewReport, export: renderReport, drill: drillReport }}
onImport={(file) => importExcel(file)}
onSaveReport={(config) => saveReportConfig({ ...config, dataModelId })}
onFontRequest={fetchFonts}
/>
);Header buttons (import / loop block / preview / export / save) are configurable via
enableImport / enablePreview / … and the customActions / extraActions slots. See
apps/app-pc/src/pages/engine.tsx for the full wiring including load-by-URL-id.
For a read-only preview page (no designer), mount ReportPreview with a loaded config and a
renderService:
import { ReportPreview } from '@coding-report/report-engine';
<ReportPreview
config={loadedConfig}
renderService={{ preview: previewReport, export: renderReport, drill: drillReport }}
onClose={() => navigate('/reports')}
/>The whole flow — parameter modal → render → preview drawer → drill-down → in-drawer export — is encapsulated. The designer and the standalone page share this same component.
The demo app exposes five routes; replicate as needed:
| Route | Component | Purpose |
|---|---|---|
/datasource-types |
driver management | register JDBC driver jars |
/datasources |
DataSourceManager |
connections + datasets (physical & SQL) |
/datamodels |
DataModelListPage |
data models (datasets / unions / relations / transforms) |
/reports |
report list (antd Table) | create / edit / preview / delete |
/engine, /preview |
ReportEngine / ReportPreview |
entered from the report list |
Wrap your app in antd's ConfigProvider (+ App) as the demo does in
apps/app-pc/src/index.tsx.
UniverSheet requests fonts through the onFontRequest callback you pass down (the demo uses
fetchFonts from report-api). The wrapper handles localStorage caching, @font-face
injection, and Univer registration internally — you only return the font list.
- Display alias vs real id: cells show aliases but transmit real ids.
value.payloadis the authoritative id (used for transport/export);displayTextis a transient alias for display only and is never sent to the backend. Always derive display fromvalue, never reverse adisplayTextback into a value. - Enum values are UPPERCASE string unions (
'VERTICAL','SUM','FieldValue') aligned to the Java enumname(). - Data transforms: bind a dictionary in a cell via the "数据转换" category in the expression
builder, which inserts
map(field, "transformId"). Transform items are configured under a data model (the designer's transforms tab). Valuehas no Jackson polymorphism on the backend — transport uses DTO records, so the frontend types mirror those DTOs exactly. When adding a field toCellBinding, update all layers (see the projectCLAUDE.mdfor the five-place checklist).
apps/app-pc is the canonical working integration.
cd report-frontend
pnpm install
pnpm build # builds report-univer → report-api → report-engine → app-pc
pnpm dev:app-pc # starts the demo app (proxies /api to localhost:8090)
pnpm test # library unit tests (rstest)
{ "dependencies": { "@coding-report/report-univer": "0.0.1", "@coding-report/report-api": "0.0.1", "@coding-report/report-engine": "0.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "antd": "^6.2.1", "axios": "^1.7.9" } }