JavaScript Frontend Viewer
ReportShell provides a JavaScript web report viewer built with React and Material UI. It serves both as an end-user report viewing UI and as a concrete example of how to consume the ReportShell REST APIs.
Capabilities
Section titled “Capabilities”The React viewer covers several of the most common flows supported by the ReportShell REST APIs, but it does not implement every capability exposed by the APIs. It is not a full-featured, all-purpose report viewer.
Dynamic Parameter Forms
Section titled “Dynamic Parameter Forms”The viewer automatically generates user-friendly forms based on structured input control metadata typically parsed from your .jrxml parameter metadata.
- Type-Aware Inputs: Renders appropriate controls for Strings, Numbers, Dates, and Booleans.
- Cascading Selections: Handles dependent parameters (e.g., picking a “Country” filters the “City” list) seamlessly using the Input Controls REST API.
- Validation: Handles basic format validation for numbers and dates, and highlights required controls.
Report Presentation
Section titled “Report Presentation”- HTML and PDF previews: Run and preview reports in paginated or unpaginated mode.
- Zoom Controls: Adjust the zoom ratio for optimal readability. Note that changing the zoom re-runs (fills) the report with the parameters used for the last report run. This might cause slow response times for large reports. Once zoom is changed, subsequent runs use the same zoom setting.
Export Management
Section titled “Export Management”- Exports: Download reports in PDF, Excel, CSV, and other formats supported by the JasperReports Library exporters.
- Export Options: Select standard export options such as paginated vs unpaginated exports, horizontal/vertical offset, and the one page per sheet option for Excel exports.
Execution Flow and Error Handling
Section titled “Execution Flow and Error Handling”The viewer requires running the report as an HTML preview first. It POSTs the parameter values to the Render REST API and displays the resulting HTML output inside an <iframe>. Only after a successful HTML preview can the user export the same report to another format (PDF, DOCX, XLSX, etc.); each export is a separate POST to the Render API with the same parameters.
Because parameters are submitted via a form POST targeting the iframe, the viewer does not see the HTTP response and cannot intercept errors from the render endpoint. Any failure (e.g., parameter binding errors, session expiry, authorization failures, report execution errors) is rendered by the server as the iframe’s page content.
To make these errors useful to end users, handle the relevant ReportShell exception types on the server side (for example, with a @ExceptionHandler, @ControllerAdvice or custom error view) and render appropriate details such as validation messages, a re-authentication prompt, or a friendly failure page. The viewer will simply display whatever the server returns inside the iframe.
Distribution
Section titled “Distribution”The React viewer is distributed as a precompiled ES module bundle, not as source code. You import and mount this bundle in your own application, and treat it as an integration dependency rather than application code you modify directly.
Customization is intentionally limited:
- You can tweak behavior and appearance only through the exposed configuration/settings when mounting the viewer.
- For deeply customized, hardened, or highly specific UIs, you should build your own frontend on top of the ReportShell REST APIs instead of trying to adapt the bundled viewer.
Configuration
Section titled “Configuration”The viewer uses two configuration layers:
- Initialization settings from
init(...) - Per-mount props passed to
mountReportViewer(...)
Importing the Viewer Module
Section titled “Importing the Viewer Module”The viewer is distributed as an ES module. Import it first, then prepare global configuration, then mount it.
Example import:
const viewerModule = await import( "/reportshell/assets/reportshell-viewer-react-latest.js");This module URL works out of the box because the reportshell-web JAR packages the viewer bundle as a static resource under META-INF/resources/reportshell/assets/. Spring MVC serves resources from META-INF/resources/** automatically, so the browser can load the module directly from /reportshell/assets/....
The locale modules explained below are served the same way. They are also static resources packaged by the reportshell-web JAR and exposed through /reportshell/assets/i18n/....
Initialization
Section titled “Initialization”Use init() for settings that are shared by the mounted viewer instance. This method takes a JavaScript
object with the following properties:
restApiBaseUri: Base URI for the ReportShell REST API (for example,/reportshell/api/v1)timeZone: Server/application business time zone as an IANA timezone ID.exportFormats: Array of export format descriptors used by the viewer UI. Each item hastitle,id, andextension.locale: A JS object with properties:language,country,scriptandvariantrepresenting a JavaLocaleobject’s parts separately. This value is used to select best matching bundled locale from the supported translations. If a matching bundle is not found, this method returns the bundle for English language (en.js).
Example global configuration:
const viewerModule = await import( "/reportshell/assets/reportshell-viewer-react-latest.js");
await viewerModule.init({ viewerApiBaseUrl: "/reportshell/api/v1", timeZone: "Europe/Paris", exportFormats: [ { title: "PDF Document", id: "pdf", extension: ".pdf" }, { title: "HTML", id: "html", extension: ".html" }, { title: "RTF", id: "rtf", extension: ".rtf" }, { title: "CSV", id: "csv", extension: ".csv" }, { title: "Excel", id: "xlsx", extension: ".xlsx" }, { title: "Word", id: "docx", extension: ".docx" }, ],})Mount props
Section titled “Mount props”Pass report-specific options to mountReportViewer(...):
reportKey: The report key to render. See Report Resolution for how report keys work.title: Report title shown in the viewer.exportFormats: Array of enabled format ids for this mounted report, for example["pdf", "html", "rtf", "csv", "xlsx", "docx"]previewFormat: Format the viewer renders when the user clicks Run Report. Either"html"(the default, interactive HTML preview) or"pdf"(the report opens directly in the browser’s built-in PDF viewer). When set to"pdf", the HTML zoom and pagination controls are hidden because the browser PDF viewer provides its own equivalents, and PDF is removed from the Export menu since exporting to the format already on screen would be redundant.locales: Optional list of{ key, label }items shown in the viewer’s settings panel as a runtime override for the report’s effective locale. When omitted or empty, the locale picker is disabled. The host app curates this list to the locales it has translations for; the viewer never enumerates the JVM’s available locales itself.timezones: Optional list of{ key, label }items (IANA timezone IDs) shown in the viewer’s settings panel as a runtime override for the report’s effective timezone. When omitted or empty, the timezone picker is disabled.requestInterceptor: Optional callback invoked before every request the viewer issues (controls, control-states, render, export) so you can add custom query params, headers, orrs.extra.*parameters. See Request Interceptor below.theme: Optional Material UI theme customization callback. See Theme Customization below.
Example mount call:
const viewerModule = await import( "/reportshell/assets/reportshell-viewer-react-1.0.0.js");
viewerModule.mountReportViewer(document.getElementById("root"), { reportKey: "samples/sales", title: "Sales Report", exportFormats: ["pdf", "html", "rtf", "csv", "xlsx", "docx"], previewFormat: "pdf", locales: [ { key: "en", label: "English" }, { key: "fr", label: "Français" }, ], timezones: [ { key: "Europe/Paris", label: "Europe/Paris" }, { key: "America/New_York", label: "America/New_York" }, ],});Request Interceptor
Section titled “Request Interceptor”Pass a requestInterceptor mount prop to inspect or mutate every request the viewer issues right before it is dispatched. The same callback is invoked for control loading, cascading control-state lookups, the render call that backs the iframe, and export downloads. Typical uses include attaching tenant headers, adding correlation IDs, and appending rs.extra.* parameters that flow into your custom factories or interceptors on the server (see Custom Factories).
viewerModule.mountReportViewer(document.getElementById("root"), { reportKey: "samples/sales", requestInterceptor: (request) => { // Add a header (effective for fetch-based requests). request.headers.set("X-Tenant-Id", currentTenantId());
// Append an extra parameter to render/export requests. request.body?.formData?.append("rs.extra.userId", currentUserId());
// Or attach it to the URL query string. request.params.append("rs.extra.correlationId", crypto.randomUUID()); },});The callback receives a ViewerRequest object with these fields:
urlandmethod: read-only.params: aURLSearchParamsfor the URL query string. Mutate viaappend,set,delete, etc.headers: aHeadersobject. Effective for fetch-based requests only; ignored for hidden-form POST submissions (the iframe render and export flows) because browsers do not send custom headers on form submissions. Useparamsorbody.formDatafor those flows instead.body: present on POST requests. Holds eitherformData(aURLSearchParamsforapplication/x-www-form-urlencodedbodies) orraw(a value the viewer will serialize as JSON). Exactly one is set per request.
The callback runs synchronously. Asynchronous work is not supported because some flows (export popups, form-target submissions) must execute inside the originating user-gesture stack so the browser does not block them. If you need a fresh token per request, read it inside the callback rather than awaiting it.
CSRF Configuration
Section titled “CSRF Configuration”Attach your CSRF token from the request interceptor. GETs are skipped because typical CSRF filters do not protect them; every other method (POST, and any future write methods) gets the token.
viewerModule.mountReportViewer(document.getElementById("root"), { reportKey: "samples/sales", requestInterceptor: (request) => { if (request.method === "GET") return; const token = document .querySelector('meta[name="_csrf"]') ?.getAttribute("content"); if (!token) return; if (request.body?.formData) { // Reached by every current write request: iframe render POSTs, // export form submissions, and the control-states fetch. All of // them serialize as application/x-www-form-urlencoded. request.body.formData.append("_csrf", token); } else { // Not reached by today's viewer, but kept as a safety net for any // future fetch-based call that uses a JSON or other non-formData // body. Form POSTs cannot carry custom headers, so we only fall // back to the header transport when the body branch does not apply. request.headers.set("X-CSRF-TOKEN", token); } },});Theme Customization (Requires a valid license)
Section titled “Theme Customization (Requires a valid license)”The viewer is built with Material UI and accepts an optional theme mount prop to customize the underlying MUI theme. Because the viewer ships as a precompiled bundle, it pins a specific MUI version at build time; your customizations apply to that bundled MUI version, not to any MUI your host application may use independently.
The theme prop is a function that receives the viewer’s base theme and returns a partial ThemeOptions object. The returned options are deep-merged into the base via createTheme(base, options), so anything you omit keeps the viewer’s defaults:
viewerModule.mountReportViewer(document.getElementById("root"), { reportKey: "samples/sales", title: "Sales Report", theme: (base) => ({ components: { MuiTextField: { defaultProps: { size: "small" }, }, MuiButton: { defaultProps: { disableElevation: true }, }, }, palette: { primary: { main: "#1565c0", }, }, }),});The full MUI Theme surface is available (palette, typography, shape, components, etc.), scoped to the MUI version the viewer bundle was built against. Common use cases include:
- Adjusting input density via
components.MuiTextField.defaultProps - Setting a consistent date picker format via
components.MuiDatePicker.defaultProps - Overriding the primary/secondary palette to match your application’s branding
- Changing default typography or font families
Bundled Locales
Section titled “Bundled Locales”The viewer ships with these locale bundles:
csdeenesfritjanlplpt-BRrutrzh-CN
These are packaged as static resources next to the viewer bundle. In the standard distribution:
- the entry bundle is served from
META-INF/resources/reportshell/assets/reportshell-viewer-react-1.0.0.js - locale chunks are served from
META-INF/resources/reportshell/assets/i18n/*.js - shared chunks are served from
META-INF/resources/reportshell/assets/chunks/*.js
Spring MVC serves META-INF/resources/** automatically, so this works out of the box in a standard Spring Boot application. If you enable Spring Security, make sure it still allows unauthenticated GET access to the viewer bundle and its related static assets.
If you serve the viewer bundle yourself, keep the bundle and the reportshell/assets/** directory structure together so the browser can resolve the locale chunks correctly.
Thymeleaf Example
Section titled “Thymeleaf Example”If you use the Report Viewer Controller, the usual flow is:
- Import the viewer bundle.
- Initialize with controller-provided
viewerEnvmodel attribute - Mount the viewer with the controller-provided
viewerProps.
If you use the Report Viewer Controller, the typical Thymeleaf integration looks like this:
<script type="module" th:inline="javascript"> const viewerModule = await import( '[(@{/reportshell/assets/reportshell-viewer-react-latest.js})]' );
// Controller populates viewerEnv prop with sensible defaults. // You may override any property here or via `ViewerCustomizer` beans. const env = /*[[(${viewerEnv})]]*/ {};
// Pass locale parts to resolve a best-match bundled locale. const env = /*[[(${viewerEnv.localeDetails})]]*/ {}; await viewerModule.init({...env, locale});
// Controller populated mount properties object with the report key, title, supported export formats const props = /*[[(${viewerProps})]]*/ {};
// Set request interceptor for CSRF or other customization props.requestInterceptor = (request) => {....}
viewerModule.mountReportViewer(document.getElementById("root"), props);</script>DateTime parsing: global timeZone
Section titled “DateTime parsing: global timeZone”The timeZone initialization setting controls how the viewer interprets and displays DateTime values.
- Type: IANA timezone ID (Java
ZoneId), for exampleEurope/Paris. - Purpose: Used when parsing DateTime values that do not include an explicit timezone/offset, and for displaying DateTime values consistently in the server/application timezone.
- Requirement: This should match the server/application timezone. If they differ, DateTime values without an offset can be interpreted incorrectly.
When submitting DateTime parameter values back to the server, the viewer serializes them with an explicit offset to keep backend binding unambiguous.
DateTime strings that already include an explicit offset (or Z) are unambiguous.
The timeZone setting is still recommended so the viewer interprets and displays DateTime values in the same timezone as the server.
The viewer intentionally uses timeZone (not the browser timezone) when interpreting DateTime values, so all users view and enter temporal values in the configured business timezone.
Integration Notes
Section titled “Integration Notes”- Init module via
await viewerModule.init(...)before themountReportViewer(...)call. - Make sure static resources for chunks and locale bundles are served relative to your viewer module script location.
Our GitHub samples demonstrates how this frontend viewer works for reports with and without parameters.
Support
Section titled “Support”This JavaScript viewer is not an officially supported end‑user product and may evolve or change over time. It should be considered “beta” quality:
- Focused on core happy‑path flows; not all REST API features or edge cases are covered.
- Has been tried with a limited set of sample reports and standard parameter types, but not broadly tested with diverse or custom parameter types.
- Includes only minimal error handling.
Despite these limitations, the viewer is fully functional for the scenarios it covers and is intended to help you get productive quickly with the REST APIs. We encourage you to use it, experiment with it, and report any issues you encounter.
Typical Frontend Flow
Section titled “Typical Frontend Flow”Whether you use the bundled React viewer or build your own UI, the frontend flow is usually:
- Initialize the parameter form Call the Input Controls REST API to retrieve available controls and their metadata.
- Handle parameter changes
When a parent control changes, call the control-states endpoint with current parameters and
rs.onchangeso dependent controls can be refreshed. - Run and render immediately Submit parameters to the Render REST API when you want a fresh fill and immediate export.
- Use persistent executions when needed (not supported by this bundled viewer) If you need reusable print metadata, paged navigation, or repeat exports from stored print data, use the Persistent Executions REST API.
© 2026 Bivektor Inc. All rights reserved. ReportShell™
is a trademark of Bivektor, Inc.
Questions? Email us at reportshell@bivektor.com.
JasperReports® and Jaspersoft® are trademarks of Cloud Software Group, Inc. and/or its subsidiaries. Eclipse BIRT™ and BIRT™ are trademarks of the Eclipse Foundation. Spring® is a trademark of Broadcom Inc. and/or its subsidiaries. React is a trademark of Meta Platforms, Inc. ReportShell and Bivektor, Inc. are not affiliated with, endorsed by, sponsored by or otherwise associated with the owners of the JasperReports®, Jaspersoft®, Spring®, Eclipse BIRT™, BIRT™ or React marks. Any reference to these or other trademarks on this site is made solely for informational, descriptive, comparative and interoperability purposes.