-
Notifications
You must be signed in to change notification settings - Fork 223
docs: Document the model visualization contract (service-module + platform layers) #2568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
oemebamo
wants to merge
1
commit into
TimefoldAI:main
Choose a base branch
from
oemebamo:timefold-solver-enterprise-issues-815
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
docs/src/modules/ROOT/pages/deploying-to-platform/visualization.adoc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| [#_platform_visualization] | ||
| = Visualization | ||
| :description: How a model's custom visualization UI is embedded and driven once deployed to Timefold Platform. | ||
| :doctype: book | ||
| :sectnums: | ||
| :icons: font | ||
|
|
||
| A model's solution can have a custom visualization UI, rendered inside Timefold Platform as an iframe, instead of consumers only seeing the raw solution data or the generic score analysis view. | ||
|
|
||
| include::_preview-note.adoc[] | ||
|
|
||
| [CAUTION] | ||
| ==== | ||
| This page documents how visualization works today, based directly on the current platform implementation. | ||
| The contract described here, including iframe sizing, refresh behavior, asset paths, and page-announcement metadata, is still evolving and may change as the platform's visualization support matures, possibly without a smooth migration path. | ||
| ==== | ||
|
|
||
| [#_building_the_ui] | ||
| == Building the UI | ||
|
|
||
| The UI itself is the same set of static files described in xref:running-timefold-solver/service/visualization.adoc[], placed under `src/main/resources/META-INF/resources`. | ||
| Once deployed, the platform repackages and serves these files under a `ui/` prefix, so the entry point the platform loads must be exactly `ui/index.html`. | ||
|
|
||
| Asset paths under this prefix are single-segment only today: `ui/main.js` is servable, but `ui/assets/main.js` is not. | ||
| Flatten your build output into a single directory, without subfolders. | ||
|
|
||
| [#_iframe_embedding] | ||
| == How the platform embeds the UI | ||
|
|
||
| The platform renders `ui/index.html` inside a fixed-size iframe, sized by the platform's own layout, with no auto-grow or resize mechanism. | ||
|
|
||
| Design your UI for a fixed viewport, or handle any overflow or scrolling yourself within that fixed area. | ||
|
|
||
| [#_calling_your_api_from_the_iframe] | ||
| == Calling your model's API from inside the iframe | ||
|
|
||
| The platform injects the following query parameters into the iframe's `src` URL: | ||
|
|
||
| [cols="1,3", options="header"] | ||
| |=== | ||
| | Parameter | Purpose | ||
|
|
||
| | `onPlatform` | ||
| | Present when the UI is running embedded in the platform, absent when the UI is opened directly. | ||
|
|
||
| | `runId` | ||
| | The identifier of the dataset being viewed. | ||
|
|
||
| | `tenantId` | ||
| | The identifier of the tenant the dataset belongs to. | ||
|
|
||
| | `apiUrl` | ||
| | The base URL to use for calls to your model's API. | ||
|
|
||
| | `apiKey` | ||
| | The API key to use for calls to your model's API. | ||
| |=== | ||
|
|
||
| Read `apiUrl` from `window.location.search`, strip any trailing slash, and prepend it to your own API calls, so they're routed correctly regardless of where the platform proxies from. | ||
| Append the path your model's own REST API is served under — the same path you'd hit locally, as described in xref:running-timefold-solver/service/visualization.adoc#_calling_your_api[Calling your REST API from the UI]. | ||
| Only the base changes between running locally and running embedded in the platform. | ||
|
|
||
| [#_refreshing_while_solving] | ||
| == Refreshing while solving | ||
|
|
||
| The platform doesn't push updates into the iframe or refresh it automatically. | ||
| Your UI needs to poll its own status or solution endpoint on an interval, and stop polling once the dataset's status leaves the active or solving set. | ||
|
|
||
| [#_error_reporting] | ||
| == Error reporting | ||
|
|
||
| The platform automatically injects a small error-forwarding script into the served HTML. | ||
| This script turns uncaught JavaScript errors and unhandled promise rejections into a `postMessage` call, which the platform surfaces to the user as an alert in its own UI. | ||
|
|
||
| Don't rely solely on `console.error` to signal failure: uncaught errors and unhandled promise rejections are what actually surface to the end user. | ||
| Anything you only log to the console stays invisible to them. | ||
|
|
||
| [#_announcing_visualization_pages] | ||
| == Announcing visualization pages | ||
|
|
||
| A model can offer multiple types of visualization, for example a map, a table, and a Gantt chart, and declares each one so the platform knows what to offer users and how to label and icon it. Declare pages through build-time configuration: | ||
|
|
||
| [source,properties,options="nowrap"] | ||
| ---- | ||
| timefold.model.visualization.pages[0].key=map | ||
| timefold.model.visualization.pages[0].icon=TbMap | ||
| timefold.model.visualization.pages[0].label=Map | ||
| timefold.model.visualization.pages[1].key=gantt | ||
| timefold.model.visualization.pages[1].icon=TbChartGantt | ||
| timefold.model.visualization.pages[1].label=Gantt chart | ||
| ---- | ||
|
|
||
| Each declared page has three required fields; omitting any of them fails the build. | ||
|
|
||
| - `key`: a stable identifier for the page. | ||
| - `icon`: an icon name from https://tabler.io/icons[Tabler Icons]. | ||
| - `label`: the human-readable name shown to users. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
docs/src/modules/ROOT/pages/running-timefold-solver/service/visualization.adoc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| [#_visualization] | ||
| = Visualization | ||
| :page-aliases: service/visualization.adoc | ||
| :description: How to build a custom visualization UI for your model and serve it locally. | ||
| :doctype: book | ||
| :sectnums: | ||
| :icons: font | ||
|
|
||
| A model's solution is often easier to understand as a rendered UI than as raw JSON. | ||
| This page describes how to build a custom visualization UI for your model and serve it from the service module while running locally. | ||
|
|
||
| See xref:deploying-to-platform/visualization.adoc[] for how this same UI is embedded once your model is deployed to Timefold Platform. | ||
|
|
||
| [#_serving_a_ui_locally] | ||
| == Serving a UI locally | ||
|
|
||
| Any static file placed under `src/main/resources/META-INF/resources` is served by Quarkus at the site root. | ||
| For example, an `index.html` and `app.js` placed there are served at `http://localhost:8080/index.html` and `http://localhost:8080/app.js`. | ||
|
|
||
| This static-resource handling is independent of your REST API path configuration: the UI files and the API endpoints are served from the same Quarkus instance, but the UI does not sit under whatever `@Path` your `ModelRest` interface declares. | ||
|
|
||
| [#_ui_support_property] | ||
| == Enabling the UI in the model descriptor archive | ||
|
|
||
| Whether the archive generated around your xref:deploying-to-platform/guide.adoc#_what_happens_on_deploy[model descriptor] (`model-descriptor.zip`) bundles a UI is controlled by the build-time `timefold.model.ui-support` property, which accepts one of two values: | ||
|
|
||
| - `NONE`: no UI is bundled. | ||
| - `APP_JS`: the files under `src/main/resources/META-INF/resources` are bundled as the model's UI. | ||
|
|
||
| If you don't set this property explicitly, it's auto-detected: if `src/main/resources/META-INF/resources` exists and contains at least one file, `APP_JS` is used; otherwise, `NONE` is used. | ||
|
|
||
| [#_tips] | ||
| == Tips | ||
|
|
||
| [#_relative_asset_paths] | ||
| === Use relative asset paths | ||
|
|
||
| When deployed to Timefold Platform, the same `META-INF/resources` files are repackaged and served under a `ui/` prefix instead of the site root (see xref:deploying-to-platform/visualization.adoc[]). | ||
| A root-absolute reference like `<script src="/app.js">` breaks once moved under that prefix; a relative one like `<script src="./app.js">` still resolves correctly. | ||
| Use relative asset paths in your `index.html` for this reason. | ||
|
|
||
| [#_cors] | ||
| === Enable CORS for external dev servers | ||
|
|
||
| The service module doesn't configure CORS for you. | ||
| To run your UI on a separate dev server (Vite, webpack, ...), add: `quarkus.http.cors=true`. | ||
|
|
||
| See the https://quarkus.io/guides/http-reference#cors-filter[Quarkus CORS guide] for how to restrict allowed origins, methods, or headers. | ||
|
|
||
| [#_calling_your_api] | ||
| == Calling your REST API from the UI | ||
|
|
||
| Your UI calls your model's REST API the same way any other client would. | ||
|
|
||
| Open the Swagger UI at `http://localhost:8080/q/swagger-ui/`, introduced in xref:quickstart/service/getting-started.adoc[Getting started: building a service], to check the exact path, rather than assuming a fixed prefix. | ||
|
|
||
| See xref:deploying-to-platform/visualization.adoc[] for how to target your API from inside the platform's iframe. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the first time I'm seeing this. Do you happen to know which model we use this for? Most model visualisations currently depend on a single page with multiple tabs.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answering my own question after digging in:
TL;DR: no model uses this, and nothing reads it either. Org-wide,
timefold.model.visualization.pagesappears only in the solver's own config class and test. On the platform side the field stops at the descriptor:ModelDescriptorDTO/RegisteredCatalogEntrynever carryvisualizationPages, and the frontend renders one hardcoded iframe gated only onuiSupport != "NONE". It shipped in #2510 to the shape the platform side requested (timefold-solver-enterprise#656), ahead of any consumer. The issue behind this PR (timefold-solver-enterprise#815) explicitly asked for that caveat to ship with this section ("say plainly it has no visible effect on the platform today"), but the section instead states the platform "knows what to offer users". Please add the caveat back.How the pieces fit (the reason "single page with multiple tabs" is what we all see today): platform-drawn tabs need two things. First, knowing which pages exist, ideally before the iframe loads. That is this metadata: the menu card. Second, actually switching pages inside the iframe without reloading it, and knowing which page is active. That is a runtime
postMessagechannel (aviewsannounce plusnavigate), which already ships in the rebuilt FSR visualization but is not documented here: https://github.com/TimefoldAI/timefold-visualisations/blob/main/docs/EMBEDDING.md. Today the menu card is printed but nobody reads it, and the waiter works but is not on this page. Every existing model therefore still draws its own tabs inside the frame.One more concrete fix for this page: the example icon
TbChartGanttdoes not exist inreact-icons5.7.0, the version the platform frontend pins, so the doc's own example would render no icon once consumption lands (measured against the package;TbMapis fine). Worth picking an icon that resolves, and the "any icon name from Tabler Icons" sentence oversells slightly.We are wiring the two halves together rather than keeping two systems, and this metadata wins the argument: the embed no longer declares its views at all (TimefoldAI/timefold-visualisations#87 removed the upward
viewsmessage), so this build-time declaration is the single source of the tab set, and the runtime channel carries only what a running embed can know — readiness, the active view, and navigation. Consumption is tracked in TimefoldAI/timefold-platform#5274 (models API) and TimefoldAI/frontend#3991 (run-page menu), and the model-side declarations in TimefoldAI/timefold-field-service-routing#1385 / TimefoldAI/timefold-employee-scheduling#1387. When a model and its visualization live in the same repository, the properties file and the page keys it must match sit side by side, which is the intended end state.