From cfafe825d047c15649a5cf48a33360d7dc65fa7a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 13 Oct 2023 01:53:17 +0000 Subject: [PATCH] Sync documentation of main branch --- _data/versioned/main/index/quarkus.yaml | 20 +- _versions/main/guides/dev-ui-v2.adoc | 1240 -------------------- _versions/main/guides/dev-ui.adoc | 1386 ++++++++++++++++++----- _versions/main/guides/optaplanner.adoc | 1100 ------------------ 4 files changed, 1116 insertions(+), 2630 deletions(-) delete mode 100644 _versions/main/guides/dev-ui-v2.adoc delete mode 100644 _versions/main/guides/optaplanner.adoc diff --git a/_data/versioned/main/index/quarkus.yaml b/_data/versioned/main/index/quarkus.yaml index 25d04b5b54..ad045f5fca 100644 --- a/_data/versioned/main/index/quarkus.yaml +++ b/_data/versioned/main/index/quarkus.yaml @@ -1254,16 +1254,16 @@ types: - io.quarkus:quarkus-redis-client type: guide url: /guides/redis-dev-services - - title: Dev UI - filename: dev-ui-v2.adoc - summary: Learn how to get your extension to contribute features to the Dev UI (v2). - categories: writing-extensions - type: guide - url: /guides/dev-ui-v2 - title: Dev UI filename: dev-ui.adoc - summary: Learn how to get your extension contribute features to the Dev UI (v1). + summary: Learn how to get your extension to contribute features to the Dev UI (v2). categories: writing-extensions + topics: + - dev-ui + - tooling + - testing + extensions: + - io.quarkus:quarkus-core type: guide url: /guides/dev-ui - title: Extending Configuration Support @@ -1761,12 +1761,6 @@ types: - io.quarkus:quarkus-oidc-client type: guide url: /guides/security-openid-connect-client - - title: OptaPlanner - Using AI to optimize a schedule with OptaPlanner - filename: optaplanner.adoc - summary: This guide walks you through the process of creating a Quarkus application with OptaPlanner's constraint solving Artificial Intelligence (AI). - categories: business-automation - type: guide - url: /guides/optaplanner - title: Packaging And Releasing With JReleaser filename: jreleaser.adoc summary: This guide covers packaging and releasing CLI applications using the JReleaser tool. diff --git a/_versions/main/guides/dev-ui-v2.adoc b/_versions/main/guides/dev-ui-v2.adoc deleted file mode 100644 index 2cc4a889d8..0000000000 --- a/_versions/main/guides/dev-ui-v2.adoc +++ /dev/null @@ -1,1240 +0,0 @@ -//// -This guide is maintained in the main Quarkus repository -and pull requests should be submitted there: -https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc -//// -= Dev UI -include::_attributes.adoc[] -:categories: writing-extensions -:summary: Learn how to get your extension to contribute features to the Dev UI (v2). - -[NOTE] -.Dev UI v2 -==== -This guide covers the Dev UI v2, which is the default from Quarkus 3 onwards. Read xref:dev-ui.adoc[Dev UI v1] for Quarkus 2.x. -==== - -This guide covers the Quarkus Dev UI for xref:building-my-first-extension.adoc[extension authors]. - -Quarkus ships with a Developer UI, which is available in dev mode (when you start -quarkus with `mvn quarkus:dev`) at http://localhost:8080/q/dev-ui[/q/dev-ui] by default. It will show you something like -this: - -image::dev-ui-overview-v2.png[alt=Dev UI overview,role="center"] - -It allows you to: - -- quickly visualize all the extensions currently loaded -- view extension statuses and go directly to extension documentation -- view and change `Configuration` -- manage and visualize `Continuous Testing` -- view `Dev Services` information -- view the Build information -- view and stream various logs - -Each extension used in the application will be listed and you can navigate to the guide for each extension, see some more information on the extension, and view configuration applicable for that extension: - -image::dev-ui-extension-card-v2.png[alt=Dev UI extension card,role="center"] - -== Make my extension extend the Dev UI - -In order to make your extension listed in the Dev UI you don't need to do anything! - -So you can always start with that :) - -Extensions can: - -- xref:add-links-to-an-extension-card[Add links to an extension card] -- xref:add-full-pages[Add full custom pages] -- xref:add-a-log-file[Add a log stream] -- xref:add-a-section-menu[Add a section menu] -- xref:custom-cards[Create a custom card] - -== Add links to an extension card - -=== External Links - -These are links that reference other (external from Dev UI) data. This data can be HTML pages, text or other data. - -A good example of this is the SmallRye OpenAPI extension that contains links to the generated openapi schema in both json and yaml format, and a link to Swagger UI: - -image::dev-ui-extension-openapi-v2.png[alt=Dev UI extension card,role="center"] - -The links to these external references is known at build time, so to get links like this on your card, all you need to do is add the following Build Step in your extension: - -[source,java] ----- -@BuildStep(onlyIf = IsDevelopment.class)// <1> -public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - - CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); // <2> - - cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") // <3> - .url(nonApplicationRootPathBuildItem.resolvePath("openapi")) // <4> - .isYamlContent() // <5> - .icon("font-awesome-solid:file-lines")); // <6> - - cardPageBuildItem.addPage(Page.externalPageBuilder("Schema json") - .url(nonApplicationRootPathBuildItem.resolvePath("openapi") + "?format=json") - .isJsonContent() - .icon("font-awesome-solid:file-code")); - - cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI") - .url(nonApplicationRootPathBuildItem.resolvePath("swagger-ui")) - .isHtmlContent() - .icon("font-awesome-solid:signs-post")); - - return cardPageBuildItem; -} ----- -<1> Always make sure that this build step is only run when in dev mode -<2> To add anything on the card, you need to return/produce a `CardPageBuildItem`. -<3> To add a link, you can use the `addPage` method, as all links go to a "page". `Page` has some builders to assist with building a page. For `external` links, use the `externalPageBuilder` -<4> Adding the url of the external link (in this case we use `NonApplicationRootPathBuildItem` to create this link, as this link is under the configurable non application path, default `/q`). Always use `NonApplicationRootPathBuildItem` if your link is available under `/q`. -<5> You can (optionally) hint the content type of the content you are navigating to. If there is no hint, a header call will be made to determine the `MediaType`; -<6> You can add an icon. All free font-awesome icons are available. - -[NOTE] -.Note about icons - -If you find your icon at https://fontawesome.com/search?o=r&m=free[Font awesome], you can map as follow: Example `` will map to `font-awesome-solid:house`, so `fa` becomes `font-awesome` and for the icon name, remove the `fa-`; - -==== Embedding external content - -By default, even external links will render inside (embedded) in Dev UI. In the case of HTML, the page will be rendered and any other content will be shown using https://codemirror.net/[code-mirror] to markup the media type. For example the open api schema document in `yaml` format: - -image::dev-ui-extension-openapi-embed-v2.png[alt=Dev UI embedded page,role="center"] - -If you do not want to embed the content, you can use the `.doNotEmbed()` on the Page Builder, this will then open the link in a new tab. - -==== Runtime external links - -The example above assumes you know the link to use at build time. There might be cases where you only know this at runtime. In that case you can use a xref:JsonRPC[JsonRPC] Method that returns the link to add, and use that when creating the link. Rather than using the `.url` method on the page builder, use the `.dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")`. - -==== Adding labels - -You can add an option label to the link in the card using one of the builder methods on the page builder. These labels can be - -- static (known at build time) `.staticLabel("staticLabelValue")` -- dynamic (loaded at runtime) `.dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")` -- streaming (continuously streaming updated values at runtime) `.streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")` - -For dynamic and streaming labels, see the xref:JsonRPC[JsonRPC] Section. - -image::dev-ui-extension-card-label-v2.png[alt=Dev UI card labels,role="center"] - -== Add full pages - -You can also link to an "internal" page (as opposed to the above "external" page). This means that you can build the page and add data and actions for rendering in Dev UI. - -=== Build time data - -To make build time data available in your full page, you can add any data to your `CardPageBuildItem` with a key and a value: - -[source,java] ----- -CardPageBuildItem pageBuildItem = new CardPageBuildItem(); -pageBuildItem.addBuildTimeData("someKey", getSomeValueObject()); ----- - -You can add multiple of these key-value pairs for all the data you know at build time that you need on the page. - -There are a few options to add full page content in Dev UI. Starting from the most basic (good start) to a full blown web-component (preferred). - -=== Display some build time data on a screen (without having to do frontend coding): - -If you have some data that is known at build time that you want to display you can use one of the following builders in `Page`: - -- xref:raw-data[Raw data] -- xref:table-data[Table data] -- xref:qute-data[Qute data] -- xref:web-component-page[Web Component page] - -==== Raw data -This will display your data in it's raw (serialised) json value: - -[source,java] ----- -cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") // <1> - .icon("font-awesome-brands:js") - .buildTimeDataKey("someKey")); // <2> ----- -<1> Use the `rawDataPageBuilder`. -<2> Link back to the key used when you added the build time data in `addBuildTimeData` on the Page BuildItem. - -That will create a link to a page that renders the raw data in json: - -image::dev-ui-raw-page-v2.png[alt=Dev UI raw page,role="center"] - -==== Table data - -You can also display your Build time data in a table if the structure allows it: - -[source,java] ----- -cardPageBuildItem.addPage(Page.tableDataPageBuilder("Table data") // <1> - .icon("font-awesome-solid:table") - .showColumn("timestamp") // <2> - .showColumn("user") // <2> - .showColumn("fullJoke") // <2> - .buildTimeDataKey("someKey")); // <3> ----- -<1> Use the `tableDataPageBuilder`. -<2> Optionally only show certain fields. -<3> Link back to the key used when you added the build time data in `addBuildTimeData` on the Page BuildItem. - -That will create a link to a page that renders the data in a table: - -image::dev-ui-table-page-v2.png[alt=Dev UI table page,role="center"] - -==== Qute data - -You can also display your build time data using a qute template. All build time data keys are available to use in the template: - -[source,java] ----- -cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") // <1> - .icon("font-awesome-solid:q") - .templateLink("qute-jokes-template.html")); // <2> ----- -<1> Use the `quteDataPageBuilder`. -<2> Link to the Qute template in `/deployment/src/main/resources/dev-ui/`. - -Using any Qute template to display the data, for example `qute-jokes-template.html`: - -[source,html] ----- - - - - - - - - - - {#for joke in jokes} // <1> - - - - - - {/for} - -
TimestampUserJoke
{joke.timestamp} {joke.user}{joke.fullJoke}
----- -<1> `jokes` added as a build time data key on the Page Build Item. - -==== Web Component page - -To build an interactive page with actions and runtime (or build time) data, you need to use the web component page: - -[source,java] ----- -cardPageBuildItem.addPage(Page.webComponentPageBuilder() // <1> - .icon("font-awesome-solid:egg") - .componentLink("qwc-arc-beans.js") // <2> - .staticLabel(String.valueOf(beans.size()))); ----- -<1> Use the `webComponentPageBuilder`. -<2> Link to the Web Component in `/deployment/src/main/resources/dev-ui/`. The title can also be defined (using `.title("My title")` in the builder), but if not the title will be assumed from the componentLink, which should always have the format `qwc` (stands for Quarkus Web Component) dash `extensionName` (example, `arc` in this case ) dash `page title` ("Beans" in this case) - -Dev UI uses https://lit.dev/[Lit] to make building these web components easier. You can read more about Web Components and Lit: - -- https://www.webcomponents.org/introduction[Web components Getting started] -- https://lit.dev/docs/[Lit documentation] - -===== Basic structure of a Web component page - -A Web component page is just a JavaScript Class that creates a new HTML Element: - -[source,javascript] ----- -import { LitElement, html, css} from 'lit'; // <1> -import { beans } from 'build-time-data'; // <2> - -/** - * This component shows the Arc Beans - */ -export class QwcArcBeans extends LitElement { // <3> - - static styles = css` // <4> - .annotation { - color: var(--lumo-contrast-50pct); // <5> - } - - .producer { - color: var(--lumo-primary-text-color); - } - `; - - static properties = { - _beans: {state: true}, // <6> - }; - - constructor() { // <7> - super(); - this._beans = beans; - } - - render() { // <8> - if (this._beans) { - return html``; - } else { - return html`No beans found`; - } - } -} -customElements.define('qwc-arc-beans', QwcArcBeans); // <10> ----- - -<1> You can import Classes and/or functions from other libraries. -In this case we use the `LitElement` class and `html` & `css` functions from `Lit` -<2> Build time data as defined in the Build step and can be imported using the key and always from `build-time-data`. All keys added in your Build step will be available. -<3> The component should be named in the following format: Qwc (stands for Quarkus Web Component) then Extension Name then Page Title, all concatenated with Camel Case. This will also match the file name format as described earlier. The component should also extend `LitComponent`. -<4> CSS styles can be added using the `css` function, and these styles only apply to your component. -<5> Styles can reference globally defined CSS variables to make sure your page renders correctly, especially when switching between light and dark mode. You can find all CSS variables in the Vaadin documentation (https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/color[Color], https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/size-space[Sizing and Spacing], etc) -<6> Properties can be added. Use `_` in front of a property if that property is private. Properties are usually injected in the HTML template, and can be defined as having state, meaning that if that property changes, the component should re-render. In this case, the beans are Build time data and only change on hot-reload, which will be covered later. -<7> Constructors (optional) should always call `super` first, and then set the default values for the properties. -<8> The render method (from `LitElement`) will be called to render the page. In this method you return the markup of the page you want. You can use the `html` function from `Lit`, that gives you a template language to output the HTML you want. Once the template is created, you only need to set/change the properties to re-render the page content. Read more about https://lit.dev/docs/components/rendering/[Lit html] -<9> You can use the built-in template functions to do conditional, list, etc. Read more about https://lit.dev/docs/templates/overview/[Lit Templates] -<10> You always need to register your Web component as a custom element, with a unique tag. Here the tag will follow the same format as the filename (`qwc` dash `extension name` dash `page title` ); - -===== Using Vaadin UI components for rendering - -Dev UI makes extensive usage of https://vaadin.com/docs/latest/components[Vaadin web components] as UI Building blocks. - -As an example, the Arc Beans are rendered using a https://vaadin.com/docs/latest/components/grid[Vaadin Grid]: - -[source,javascript] ----- -import { LitElement, html, css} from 'lit'; -import { beans } from 'build-time-data'; -import '@vaadin/grid'; // <1> -import { columnBodyRenderer } from '@vaadin/grid/lit.js'; // <2> -import '@vaadin/vertical-layout'; -import 'qui-badge'; // <3> - -/** - * This component shows the Arc Beans - */ -export class QwcArcBeans extends LitElement { - - static styles = css` - .arctable { - height: 100%; - padding-bottom: 10px; - } - - code { - font-size: 85%; - } - - .annotation { - color: var(--lumo-contrast-50pct); - } - - .producer { - color: var(--lumo-primary-text-color); - } - `; - - static properties = { - _beans: {state: true}, - }; - - constructor() { - super(); - this._beans = beans; - } - - render() { - if (this._beans) { - - return html` - - - - - - - - - - `; - - } else { - return html`No beans found`; - } - } - - _beanRenderer(bean) { - return html` - @${bean.scope.simpleName} - ${bean.nonDefaultQualifiers.map(qualifier => - html`${this._qualifierRenderer(qualifier)}` - )} - ${bean.providerType.name} - `; - } - - _kindRenderer(bean) { - return html` - - ${this._kindBadgeRenderer(bean)} - ${this._kindClassRenderer(bean)} - - `; - } - - _kindBadgeRenderer(bean){ - let kind = this._camelize(bean.kind); - let level = null; - - if(bean.kind.toLowerCase() === "field"){ - kind = "Producer field"; - level = "success"; - }else if(bean.kind.toLowerCase() === "method"){ - kind = "Producer method"; - level = "success"; - }else if(bean.kind.toLowerCase() === "synthetic"){ - level = "contrast"; - } - - return html` - ${level - ? html`${kind}` - : html`${kind}` - }`; - } - - _kindClassRenderer(bean){ - return html` - ${bean.declaringClass - ? html`${bean.declaringClass.simpleName}.${bean.memberName}()` - : html`${bean.memberName}` - } - `; - } - - _interceptorsRenderer(bean) { - if (bean.interceptors && bean.interceptors.length > 0) { - return html` - ${bean.interceptorInfos.map(interceptor => - html`
- ${interceptor.interceptorClass.name} - ${interceptor.priority} -
` - )} -
`; - } - } - - _qualifierRenderer(qualifier) { - return html`${qualifier.simpleName}`; - } - - _camelize(str) { - return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { - if (+match === 0) - return ""; - return index === 0 ? match.toUpperCase() : match.toLowerCase(); - }); - } -} -customElements.define('qwc-arc-beans', QwcArcBeans); ----- -<1> Import the Vaadin component you want to use -<2> You can also import other functions if needed -<3> There are some internal UI components that you can use, described below - -===== Using internal UI components - -Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui[internal UI components] (under the `qui` namespace) are available to make certain things easier: - -- Card -- Badge -- Alert -- Code block -- IDE Link - -====== Card - -Card component to display contents in a card - -[source,javascript] ----- -import 'qui-card'; ----- - -[source,html] ----- - -
- My contents -
-
----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L110[Example code] - -====== Badge - -Badge UI Component based on the https://vaadin.com/docs/latest/components/badge[vaadin themed] badge - -image::dev-ui-qui-badge-v2.png[alt=Dev UI Badge,role="center"] - -[source,javascript] ----- -import 'qui-badge'; ----- - -You can use any combination of small, primary, pill, with icon and clickable with any level of `default`, `success`, `warning`, `error`, `contrast` or set your own colors. - -[source,html] ----- -
-

Badges

-

Badges wrap the Vaadin theme in a component. - See https://vaadin.com/docs/latest/components/badge for more info. -

-
- -
-
- Default - Success - Warning - Error - Contrast - Custom colours -
-
-
- -
-
- Default primary - Success primary - Warning primary - Error primary - Contrast primary - Custom colours -
-
-
- -
-
- Default pill - Success pill - Warning pill - Error pill - Contrast pill - Custom colours -
-
-
- -
-
- - Default icon - - - Success icon - - - Warning icon - - - Error icon - - - Contrast icon - - - Custom colours - -
-
-
- -
-
- - - - - - -
-
-
- -
-
- this._info()}>Default - this._success()}>Success - this._warning()}>Warning - this._error()}>Error - this._contrast()}>Contrast - this._info()}>Custom colours -
-
-
-
-
----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L214[Example code] - -====== Alert - -Alerts are modeled around the Bootstrap alerts. Click https://getbootstrap.com/docs/4.0/components/alerts[here] for more info. - -Also see Notification controller below as an alternative. - -image::dev-ui-qui-alert-v2.png[alt=Dev UI Alert,role="center"] - -[source,javascript] ----- -import 'qui-alert'; ----- - -[source,html] ----- -
-
- Info alert - Success alert - Warning alert - Error alert -
- Permanent Info alert - Permanent Success alert - Permanent Warning alert - Permanent Error alert -
- Center Info alert - Center Success alert - Center Warning alert - Center Error alert -
- Info alert with icon - Success alert with icon - Warning alert with icon - Error alert with icon -
- Info alert with custom icon - Success alert with custom icon - Warning alert with custom icon - Error alert with custom icon -
- Small Info alert with icon - Small Success alert with icon - Small Warning alert with icon - Small Error alert with icon -
- Info alert with markup
quarkus.io
- Success alert with markup
quarkus.io
- Warning alert with markup
quarkus.io
- Error alert with markup
quarkus.io
-
- Primary Info alert with icon - Primary Success alert with icon - Primary Warning alert with icon - Primary Error alert with icon -
- Info alert with title - Success alert with title - Warning alert with title - Error alert with title -
- Info alert with title and icon - Success alert with title and icon - Warning alert with title and icon - Error alert with title and icon -
-
----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L316[Example code] - - -====== Code block - -Display a code block. This component is aware of the theme and will use the correct codemirror theme to match the light/dark mode. - - -image::dev-ui-qui-code-block-v2.png[alt=Dev UI Code Block,role="center"] - -[source,javascript] ----- -import 'qui-code-block'; ----- - -[source,html] ----- -
- - -
; ----- - -https://github.com/quarkusio/quarkus/blob/e03a97845738436c69443a591ec4ce88ed04ac91/extensions/kubernetes/vanilla/deployment/src/main/resources/dev-ui/qwc-kubernetes-manifest.js#L99[Example code] - -or fetching the contents from a URL: - -[source,html] ----- -
- - -
----- - -https://github.com/quarkusio/quarkus/blob/95c54fa46a6b6f31d69477234486d9359a2a3a4a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js#L116[Example code] - -====== IDE link - -Creates a link to a resource (like a Java source file) that can be opened in the user's IDE (if we could detect the IDE). - -[source,javascript] ----- -import 'qui-ide-link'; ----- - -[source,html] ----- -[${sourceClassNameFull}]; ----- - -https://github.com/quarkusio/quarkus/blob/582f1f78806d2268885faea7aa8f5a4d2b3f5b98/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js#L315[Example code] - -===== Using internal controllers - -Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller[internal controllers] are available to make certain things easier: - -- Notifier -- Storage -- Log -- Router -- JsonRPC - -====== Notifier - -This is an easy way to show a toast message. The toast can be placed on the screen (default left bottom) and can have a level (Info, Success, Warning, Error). Any of the levels can also be primary, that will create a more prominent toast message. - -See the source of this controller https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js[here]. - -Example usage: - -image::dev-ui-controller-notifier.gif[alt=Dev UI Notifier,role="center"] - -[source,javascript] ----- -import { notifier } from 'notifier'; ----- - -[source,html] ----- - this._info()}>Info; ----- - -[source,javascript] ----- -_info(position = null){ - notifier.showInfoMessage("This is an information message", position); -} ----- - -You can find all the valid positions https://vaadin.com/docs/latest/components/notification/#position[here]. - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L374[Example code] - -====== Storage - -An easy way to access the local storage in a safe way. This will store values in the local storage, scoped to your extension. This way you do not have to worry that you might clash with another extension. - -Local storage is useful to remember user preferences or state. For example, the footer remembers the state (open/close) and the size when open of the bottom drawer. - -[source,javascript] ----- -import { StorageController } from 'storage-controller'; - -// ... - -storageControl = new StorageController(this); // Passing in this will scope the storage to your extension - -// ... - -const storedHeight = this.storageControl.get("height"); // Get some value - -// ... - -this.storageControl.set('height', 123); // Set some val ----- - -https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js[Example code] - - -====== Log - -The log controller is used to add control buttons to a (footer) log. -See <>. - -image::dev-ui-log-control-v2.png[alt=Dev UI Log control,role="center"] - -[source,javascript] ----- -import { LogController } from 'log-controller'; - -// ... - -logControl = new LogController(this); // Passing in this will scope the control to your extension - -// ... -this.logControl - .addToggle("On/off switch", true, (e) => { - this._toggleOnOffClicked(e); - }).addItem("Log levels", "font-awesome-solid:layer-group", "var(--lumo-tertiary-text-color)", (e) => { - this._logLevels(); - }).addItem("Columns", "font-awesome-solid:table-columns", "var(--lumo-tertiary-text-color)", (e) => { - this._columns(); - }).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "var(--lumo-tertiary-text-color)", (e) => { - this._zoomOut(); - }).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "var(--lumo-tertiary-text-color)", (e) => { - this._zoomIn(); - }).addItem("Clear", "font-awesome-solid:trash-can", "var(--lumo-error-color)", (e) => { - this._clearLog(); - }).addFollow("Follow log", true , (e) => { - this._toggleFollowLog(e); - }).done(); ----- - -https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js[Example code] - -====== Router - -The router is mostly used internally. This uses https://github.com/vaadin/router[Vaadin Router] under the covers to route URLs to the correct page/section within the SPA. It will update the navigation and allow history (back button). This also creates the sub-menu available on extensions that have multiple pages. - -See the https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js[controller] for some methods that might be useful. - -[#JsonRPC] -====== JsonRPC - -This controller allows you to fetch or stream runtime data. (vs. <> discussed earlier). There are two parts to getting data during runtime. The Java side in the runtime module, and then the usage in the web component. - -*Java part* - -This code is responsible for making data available to display on the UI. - -You need to register the JsonPRCService in your processor in the deployment module: - -[source,java] ----- -@BuildStep(onlyIf = IsDevelopment.class)// <1> -JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {// <2> - return new JsonRPCProvidersBuildItem(CacheJsonRPCService.class);// <3> -} ----- -<1> Always only do this in Dev Mode -<2> Produce / return a `JsonRPCProvidersBuildItem` -<3> Define the class in your runtime module that will contain methods that make data available in the UI - -https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/devui/CacheDevUiProcessor.java[Example code] - -Now, in your Runtime module create the JsonRPC Service. This class will default to an application scoped bean, except if you explicitly scope the bean. All public methods that return something will be made available to call from the Web component Javascript. - -The return object in these methods can be: - -- primitives or `String`, -- `io.vertx.core.json.JsonArray` -- `io.vertx.core.json.JsonObject` -- any other POJO that can be serializable to Json - -All of the above can be blocking (POJO) or non-blocking (`@NonBlocking` or `Uni`). Or alternatively data can be streamed using `Multi`. - -[source,java] ----- -@NonBlocking // <1> -public JsonArray getAll() { // <2> - Collection names = manager.getCacheNames(); - List allCaches = new ArrayList<>(names.size()); - for (String name : names) { - Optional cache = manager.getCache(name); - if (cache.isPresent() && cache.get() instanceof CaffeineCache) { - allCaches.add((CaffeineCache) cache.get()); - } - } - allCaches.sort(Comparator.comparing(CaffeineCache::getName)); - - var array = new JsonArray(); - for (CaffeineCache cc : allCaches) { - array.add(getJsonRepresentationForCache(cc)); - } - return array; -} ----- -<1> This example runs non blocking. We could also return `Uni` -<2> The method name `getAll` will be available in the Javascript - -https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devconsole/CacheJsonRPCService.java[Example code] - -*Webcomponent (Javascript) part* - -Now you can use the JsonRPC controller to access the `getAll` method (and any other methods in you JsonRPC Service) - -[source,javascript] ----- -import { JsonRpc } from 'jsonrpc'; - -// ... - -jsonRpc = new JsonRpc(this); // Passing in this will scope the rpc calls to your extension - -// ... - -/** - * Called when displayed - */ -connectedCallback() { - super.connectedCallback(); - this.jsonRpc.getAll().then(jsonRpcResponse => { // <1> - this._caches = new Map(); - jsonRpcResponse.result.forEach(c => { //<2> - this._caches.set(c.name, c); - }); - }); -} ----- - -<1> Note the method `getAll` corresponds to the method in your Java Service. This method returns a https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise[Promise] with the JsonRPC result. -<2> In this case the result is an array, so we can loop over it. - -JsonArray (or any Java collection) in either blocking or non-blocking will return an array, else a JsonObject will be returned. - -https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js[Example code] - -You can also pass in parameters in the method being called, for example: -(In the Runtime Java code) - -[source,java] ----- -public Uni clear(String name) { //<1> - Optional cache = manager.getCache(name); - if (cache.isPresent()) { - return cache.get().invalidateAll().map((t) -> getJsonRepresentationForCache(cache.get())); - } else { - return Uni.createFrom().item(new JsonObject().put("name", name).put("size", -1)); - } -} ----- -<1> the clear method takes one parameter called `name` - -In the Webcomponent (Javascript): - -[source,javascript] ----- -_clear(name) { - this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { //<1> - this._updateCache(jsonRpcResponse.result) - }); -} ----- -<1> the `name` parameter is passed in. - -====== Streaming data - -You can keep a UI screen updated with the latest data by continuously streaming data to the screen. This can be done with `Multi` (Java side) and `Observer` (Javascript side) - -Java side of streaming data: - -[source,java] ----- -public class JokesJsonRPCService { - - private final BroadcastProcessor jokeStream = BroadcastProcessor.create(); - - @PostConstruct - void init() { - Multi.createFrom().ticks().every(Duration.ofHours(4)).subscribe().with((item) -> { - jokeStream.onNext(getJoke()); - }); - } - - public Multi streamJokes() { // <1> - return jokeStream; - } - - // ... -} ----- -<1> Return the Multi that will stream jokes - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/runtime/src/main/java/io/quarkus/jokes/runtime/JokesJsonRPCService.java#L37[Example code] - -Javascript side of streaming data: - -[source,javascript] ----- -this._observer = this.jsonRpc.streamJokes().onNext(jsonRpcResponse => { //<1> - this._addToJokes(jsonRpcResponse.result); - this._numberOfJokes = this._numberOfJokes++; -}); - -// ... - -this._observer.cancel(); //<2> ----- -<1> You can call the method (optionally passing in parameters) and then provide the code that will be called on the next event. -<2> Make sure to keep an instance of the observer to cancel later if needed. - -https://github.com/phillip-kruger/quarkus-jokes/blob/main/deployment/src/main/resources/dev-ui/qwc-jokes-web-components.js[Example code] - -====== Dev UI Log - -When running a local application using the `999-SNAPSHOT` version, the Dev UI will show a `Dev UI` Log in the footer. This is useful to debug all JSON RPC messages flowing between the browser and the Quarkus app. - -image::dev-ui-jsonrpc-log-v2.png[alt=Dev UI Json RPC Log,role="center"] - -== Hot reload - -You can update a screen automatically when a Hot reload has happened. To do this replace the `LitElement` that your Webcomponent extends with `QwcHotReloadElement`. - -`QwcHotReloadElement` extends `LitElement` so your component is still a Lit Element. - -When extending a `QwcHotReloadElement` you have to provide the `hotReload` method. (You also still need to provide the `render` method from Lit) - -[source,javascript] ----- -import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; - -// ... - -export class QwcMyExtensionPage extends QwcHotReloadElement { - - render(){ - // ... - } - - hotReload(){ - // .. - } - -} ----- - -== Custom cards - -You can customize the card that is being displayed on the extension page if you do not want to use the default built-in card. - -To do this, you need to provide a Webcomponent that will be loaded in the place of the provided card and register this in the Java Processor: - -[source,java] ----- -cardPageBuildItem.setCustomCard("qwc-mycustom-card.js"); ----- - -On the Javascript side, you have access to all the pages (in case you want to create links) - -[source,javascript] ----- -import { pages } from 'build-time-data'; ----- - -And the following properties will be passed in: - -- extensionName -- description -- guide -- namespace - -[source,javascript] ----- -static properties = { - extensionName: {type: String}, - description: {type: String}, - guide: {type: String}, - namespace: {type: String} -} ----- - -== State (Advance) - -State allows properties to contain state and can be reused globally. An example of state properties are the theme, the connection state (if we are connected to the backend), etc. - -See the https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state[current built-in] state objects. - -The state in Dev UI uses https://github.com/gitaarik/lit-state[LitState] and you can read more about it in their https://gitaarik.github.io/lit-state/build/[documentation]. - - -== Add a log file - -Apart from adding a card and a page, extensions can add a log to the footer. This is useful to log things happening continuously. A page will lose connection to the backend when navigating away from that page, a log in the footer is permanently connected. - -Adding something to the footer works exactly like adding a Card, except you use a `FooterPageBuildItem` rather than a `CardPageBuildItem`. - -[source,java] ----- -FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem(); - -footerPageBuildItem.addPage(Page.webComponentPageBuilder() - .icon("font-awesome-regular:face-grin-tongue-wink") - .title("Joke Log") - .componentLink("qwc-jokes-log.js")); - -footerProducer.produce(footerPageBuildItem); ----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/java/io/quarkus/jokes/deployment/devui/JokesDevUIProcessor.java#L87[Example code] - -In your Webcomponent you can then stream the log to the UI: - -[source,javascript] ----- -export class QwcJokesLog extends LitElement { - jsonRpc = new JsonRpc(this); - logControl = new LogController(this); - - // .... -} ----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/main/deployment/src/main/resources/dev-ui/qwc-jokes-log.js[Example code] - -== Add a section menu - -This allows an extension to link a page directly in the section Menu. - -Adding something to the section menu works exactly like adding a Card, except you use a `MenuPageBuildItem` rather than a `CardPageBuildItem`. - -[source,java] ----- -MenuPageBuildItem menuPageBuildItem = new MenuPageBuildItem(); - -menuPageBuildItem.addPage(Page.webComponentPageBuilder() - .icon("font-awesome-regular:face-grin-tongue-wink") - .title("One Joke") - .componentLink("qwc-jokes-menu.js")); - -menuProducer.produce(menuPageBuildItem); ----- - -https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/java/io/quarkus/jokes/deployment/devui/JokesDevUIProcessor.java#LL71C16-L71C16[Example code] - -Your page can be any Page similar to Cards. - -== Testing - -You can add tests to your extension that test: - -- Build time data -- Runtime data via JsonRPC - -You need to add this to your pom: - -[source,xml] ----- - - io.quarkus - quarkus-vertx-http-dev-ui-tests - test - ----- - -This will give you access to two base classes for creating these tests. - -=== Testing Build time data - -If you added Build time data, for example: - -[source,java] ----- -cardPageBuildItem.addBuildTimeData("somekey", somevalue); ----- - -To test that your build time data is generated correctly you can add a test that extends `DevUIBuildTimeDataTest`. - -[source,java] ----- -public class SomeTest extends DevUIBuildTimeDataTest { - - @RegisterExtension - static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication(); - - public SomeTest() { - super("io.quarkus.my-extension"); - } - - @Test - public void testSomekey() throws Exception { - JsonNode somekeyResponse = super.getBuildTimeData("somekey"); - Assertions.assertNotNull(somekeyResponse); - - // Check more values on somekeyResponse - } - -} ----- - -=== Testing Runtime data - -If you added a JsonRPC Service with runtime data responses, for example: - -[source,java] ----- -public boolean updateProperties(String content, String type) { - // ... -} ----- - -To test that `updateProperties` execute correctly via JsonRPC you can add a test that extends `DevUIJsonRPCTest`. - -[source,java] ----- -public class SomeTest extends DevUIJsonRPCTest { - - @RegisterExtension - static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication(); - - public SomeTest() { - super("io.quarkus.my-extension"); - } - - @Test - public void testUpdateProperties() throws Exception { - - JsonNode updatePropertyResponse = super.executeJsonRPCMethod("updateProperty", - Map.of( - "name", "quarkus.application.name", - "value", "changedByTest")); - Assertions.assertTrue(updatePropertyResponse.asBoolean()); - - // Get the properties to make sure it is changed - JsonNode allPropertiesResponse = super.executeJsonRPCMethod("getAllValues"); - String applicationName = allPropertiesResponse.get("quarkus.application.name").asText(); - Assertions.assertEquals("changedByTest", applicationName); - } -} ----- diff --git a/_versions/main/guides/dev-ui.adoc b/_versions/main/guides/dev-ui.adoc index bdc51abba6..bd1f4088be 100644 --- a/_versions/main/guides/dev-ui.adoc +++ b/_versions/main/guides/dev-ui.adoc @@ -6,405 +6,1237 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc = Dev UI include::_attributes.adoc[] :categories: writing-extensions -:summary: Learn how to get your extension contribute features to the Dev UI (v1). +:summary: Learn how to get your extension to contribute features to the Dev UI (v2). +:topics: dev-ui,tooling,testing +:extensions: io.quarkus:quarkus-core [NOTE] -.Dev UI v1 +.Dev UI v2 ==== -This guide covers the Dev UI v1, which has been replaced in Quarkus 3 with xref:dev-ui-v2.adoc[a new Dev UI]. -You can still access the Dev UI v1 using http://localhost:8080/q/dev-v1[/q/dev-v1] +This guide covers the Dev UI v2, which is the default from Quarkus 3 onwards. ==== This guide covers the Quarkus Dev UI for xref:building-my-first-extension.adoc[extension authors]. -Quarkus ships with a new experimental Dev UI, which is available in dev mode (when you start -quarkus with `mvn quarkus:dev`) at http://localhost:8080/q/dev-v1[/q/dev-v1] by default. It will show you something like +Quarkus ships with a Developer UI, which is available in dev mode (when you start +quarkus with `mvn quarkus:dev`) at http://localhost:8080/q/dev-ui[/q/dev-ui] by default. It will show you something like this: -image::dev-ui-overview.png[alt=Dev UI overview,role="center",width=90%] +image::dev-ui-overview-v2.png[alt=Dev UI overview,role="center"] -It allows you to quickly visualize all the extensions currently loaded, see their status and go directly -to their documentation. +It allows you to: -On top of that, each extension can add: +- quickly visualize all the extensions currently loaded +- view extension statuses and go directly to extension documentation +- view and change `Configuration` +- manage and visualize `Continuous Testing` +- view `Dev Services` information +- view the Build information +- view and stream various logs -- xref:how-can-i-make-my-extension-support-the-dev-ui[Custom useful bits of runtime information in the overview] -- xref:adding-full-pages[Full custom pages] -- xref:advanced-usage-adding-actions[Interactive pages with actions] +Each extension used in the application will be listed and you can navigate to the guide for each extension, see some more information on the extension, and view configuration applicable for that extension: -== How can I make my extension support the Dev UI? +image::dev-ui-extension-card-v2.png[alt=Dev UI extension card,role="center"] + +== Make my extension extend the Dev UI In order to make your extension listed in the Dev UI you don't need to do anything! So you can always start with that :) -If you want to contribute badges or links in your extension card on the Dev UI overview -page, like this: +Extensions can: -image:dev-ui-embedded.png[alt=Dev UI embedded,role="center"] +- xref:add-links-to-an-extension-card[Add links to an extension card] +- xref:add-full-pages[Add full custom pages] +- xref:add-a-log-file[Add a log stream] +- xref:add-a-section-menu[Add a section menu] +- xref:custom-cards[Create a custom card] -You have to add a file named `dev-templates/embedded.html` in your -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`deployment`] -extension module's resources: +== Add links to an extension card -image::dev-ui-embedded-file.png[alt=Dev UI embedded.html,align=center] +=== External Links -The contents of this file will be included in your extension card, so for example we can place -two links with some styling and icons: +These are links that reference other (external from Dev UI) data. This data can be HTML pages, text or other data. -[source,html] +A good example of this is the SmallRye OpenAPI extension that contains links to the generated openapi schema in both json and yaml format, and a link to Swagger UI: + +image::dev-ui-extension-openapi-v2.png[alt=Dev UI extension card,role="center"] + +The links to these external references is known at build time, so to get links like this on your card, all you need to do is add the following Build Step in your extension: + +[source,java] ---- - - - OpenAPI -
- - - Swagger UI +@BuildStep(onlyIf = IsDevelopment.class)// <1> +public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); // <2> + + cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") // <3> + .url(nonApplicationRootPathBuildItem.resolvePath("openapi")) // <4> + .isYamlContent() // <5> + .icon("font-awesome-solid:file-lines")); // <6> + + cardPageBuildItem.addPage(Page.externalPageBuilder("Schema json") + .url(nonApplicationRootPathBuildItem.resolvePath("openapi") + "?format=json") + .isJsonContent() + .icon("font-awesome-solid:file-code")); + + cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI") + .url(nonApplicationRootPathBuildItem.resolvePath("swagger-ui")) + .isHtmlContent() + .icon("font-awesome-solid:signs-post")); + + return cardPageBuildItem; +} ---- +<1> Always make sure that this build step is only run when in dev mode +<2> To add anything on the card, you need to return/produce a `CardPageBuildItem`. +<3> To add a link, you can use the `addPage` method, as all links go to a "page". `Page` has some builders to assist with building a page. For `external` links, use the `externalPageBuilder` +<4> Adding the url of the external link (in this case we use `NonApplicationRootPathBuildItem` to create this link, as this link is under the configurable non application path, default `/q`). Always use `NonApplicationRootPathBuildItem` if your link is available under `/q`. +<5> You can (optionally) hint the content type of the content you are navigating to. If there is no hint, a header call will be made to determine the `MediaType`; +<6> You can add an icon. All free font-awesome icons are available. + +[NOTE] +.Note about icons + +If you find your icon at https://fontawesome.com/search?o=r&m=free[Font awesome], you can map as follow: Example `` will map to `font-awesome-solid:house`, so `fa` becomes `font-awesome` and for the icon name, remove the `fa-`; + +==== Embedding external content + +By default, even external links will render inside (embedded) in Dev UI. In the case of HTML, the page will be rendered and any other content will be shown using https://codemirror.net/[code-mirror] to markup the media type. For example the open api schema document in `yaml` format: + +image::dev-ui-extension-openapi-embed-v2.png[alt=Dev UI embedded page,role="center"] + +If you do not want to embed the content, you can use the `.doNotEmbed()` on the Page Builder, this will then open the link in a new tab. + +==== Runtime external links + +The example above assumes you know the link to use at build time. There might be cases where you only know this at runtime. In that case you can use a xref:JsonRPC[JsonRPC] Method that returns the link to add, and use that when creating the link. Rather than using the `.url` method on the page builder, use the `.dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")`. -TIP: We use the Font Awesome Free icon set. +==== Adding labels -Note how the paths are specified: `{config:http-path('quarkus.smallrye-openapi.path')}`. This is a special -directive that the quarkus dev console understands: it will replace that value with the resolved route -named 'quarkus.smallrye-openapi.path'. +You can add an option label to the link in the card using one of the builder methods on the page builder. These labels can be -The corresponding non-application endpoint is declared using `.routeConfigKey` to associate the route with a name: +- static (known at build time) `.staticLabel("staticLabelValue")` +- dynamic (loaded at runtime) `.dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")` +- streaming (continuously streaming updated values at runtime) `.streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")` + +For dynamic and streaming labels, see the xref:JsonRPC[JsonRPC] Section. + +image::dev-ui-extension-card-label-v2.png[alt=Dev UI card labels,role="center"] + +== Add full pages + +You can also link to an "internal" page (as opposed to the above "external" page). This means that you can build the page and add data and actions for rendering in Dev UI. + +=== Build time data + +To make build time data available in your full page, you can add any data to your `CardPageBuildItem` with a key and a value: [source,java] ---- - nonApplicationRootPathBuildItem.routeBuilder() - .route(openApiConfig.path) // <1> - .routeConfigKey("quarkus.smallrye-openapi.path") // <2> - ... - .build(); +CardPageBuildItem pageBuildItem = new CardPageBuildItem(); +pageBuildItem.addBuildTimeData("someKey", getSomeValueObject()); ---- -<1> The configured path is resolved into a valid route. -<2> The resolved route path is then associated with the key `quarkus.smallrye-openapi.path`. -== Path considerations +You can add multiple of these key-value pairs for all the data you know at build time that you need on the page. -Paths are tricky business. Keep the following in mind: +There are a few options to add full page content in Dev UI. Starting from the most basic (good start) to a full blown web-component (preferred). -* Assume your UI will be nested under the dev endpoint. Do not provide a way to customize this without a strong reason. -* Never construct your own absolute paths. Adding a suffix to a known, normalized and resolved path is fine. +=== Display some build time data on a screen (without having to do frontend coding): -Configured paths, like the `dev` endpoint used by the console or the SmallRye OpenAPI path shown in the example above, -need to be properly resolved against both `quarkus.http.root-path` and `quarkus.http.non-application-root-path`. -Use `NonApplicationRootPathBuildItem` or `HttpRootPathBuildItem` to construct endpoint routes and identify resolved -path values that can then be used in templates. +If you have some data that is known at build time that you want to display you can use one of the following builders in `Page`: -The `{devRootAppend}` variable can also be used in templates to construct URLs for static dev console resources, for example: +- xref:raw-data[Raw data] +- xref:table-data[Table data] +- xref:qute-data[Qute data] +- xref:web-component-page[Web Component page] -[source,html] +==== Raw data +This will display your data in it's raw (serialised) json value: + +[source,java] ---- -Quarkus +cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") // <1> + .icon("font-awesome-brands:js") + .buildTimeDataKey("someKey")); // <2> ---- +<1> Use the `rawDataPageBuilder`. +<2> Link back to the key used when you added the build time data in `addBuildTimeData` on the Page BuildItem. -Refer to the xref:all-config.adoc#quarkus-vertx-http_quarkus.http.non-application-root-path[Quarkus Vertx HTTP configuration reference] -for details on how the non-application root path is configured. +That will create a link to a page that renders the raw data in json: -== Template and styling support +image::dev-ui-raw-page-v2.png[alt=Dev UI raw page,role="center"] -Both the `embedded.html` files and any full page you add in `/dev-templates` will be interpreted by -xref:qute.adoc[the Qute template engine]. +==== Table data -This also means that you can xref:qute-reference.adoc#user_tags[add custom Qute tags] in -`/dev-templates/tags` for your templates to use. +You can also display your Build time data in a table if the structure allows it: -The style system currently in use is https://getbootstrap.com/docs/4.6/getting-started/introduction/[Bootstrap V4 (4.6.0)] -but note that this might change in the future. +[source,java] +---- +cardPageBuildItem.addPage(Page.tableDataPageBuilder("Table data") // <1> + .icon("font-awesome-solid:table") + .showColumn("timestamp") // <2> + .showColumn("user") // <2> + .showColumn("fullJoke") // <2> + .buildTimeDataKey("someKey")); // <3> +---- +<1> Use the `tableDataPageBuilder`. +<2> Optionally only show certain fields. +<3> Link back to the key used when you added the build time data in `addBuildTimeData` on the Page BuildItem. + +That will create a link to a page that renders the data in a table: -The main template also includes https://jquery.com/[jQuery 3.5.1], but here again this might change. +image::dev-ui-table-page-v2.png[alt=Dev UI table page,role="center"] -=== Accessing Config Properties +==== Qute data -A `config:property(name)` expression can be used to output the config value for the given property name. -The property name can be either a string literal or obtained dynamically by another expression. -For example `{config:property('quarkus.lambda.handler')}` and `{config:property(foo.propertyName)}`. +You can also display your build time data using a qute template. All build time data keys are available to use in the template: -Reminder: do not use this to retrieve raw configured path values. As shown above, use `{config:http-path(...)}` with -a known route configuration key when working with resource paths. +[source,java] +---- +cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") // <1> + .icon("font-awesome-solid:q") + .templateLink("qute-jokes-template.html")); // <2> +---- +<1> Use the `quteDataPageBuilder`. +<2> Link to the Qute template in `/deployment/src/main/resources/dev-ui/`. -== Adding full pages +Using any Qute template to display the data, for example `qute-jokes-template.html`: -To add full pages for your Dev UI extension such as this one: +[source,html] +---- + + + + + + + + + + {#for joke in jokes} // <1> + + + + + + {/for} + +
TimestampUserJoke
{joke.timestamp} {joke.user}{joke.fullJoke}
+---- +<1> `jokes` added as a build time data key on the Page Build Item. -image::dev-ui-page.png[alt=Dev UI custom page,align=center,width=90%] +==== Web Component page -You need to place them in your extension's -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`deployment`] module's -`/dev-templates` resource folder, like this page for the xref:cache.adoc[`quarkus-cache` extension]: +To build an interactive page with actions and runtime (or build time) data, you need to use the web component page: -[[action-example]] [source,java] ---- -{#include main}// <1> - {#style}// <2> - .custom { - color: gray; +cardPageBuildItem.addPage(Page.webComponentPageBuilder() // <1> + .icon("font-awesome-solid:egg") + .componentLink("qwc-arc-beans.js") // <2> + .staticLabel(String.valueOf(beans.size()))); +---- +<1> Use the `webComponentPageBuilder`. +<2> Link to the Web Component in `/deployment/src/main/resources/dev-ui/`. The title can also be defined (using `.title("My title")` in the builder), but if not the title will be assumed from the componentLink, which should always have the format `qwc` (stands for Quarkus Web Component) dash `extensionName` (example, `arc` in this case ) dash `page title` ("Beans" in this case) + +Dev UI uses https://lit.dev/[Lit] to make building these web components easier. You can read more about Web Components and Lit: + +- https://www.webcomponents.org/introduction[Web components Getting started] +- https://lit.dev/docs/[Lit documentation] + +===== Basic structure of a Web component page + +A Web component page is just a JavaScript Class that creates a new HTML Element: + +[source,javascript] +---- +import { LitElement, html, css} from 'lit'; // <1> +import { beans } from 'build-time-data'; // <2> + +/** + * This component shows the Arc Beans + */ +export class QwcArcBeans extends LitElement { // <3> + + static styles = css` // <4> + .annotation { + color: var(--lumo-contrast-50pct); // <5> + } + + .producer { + color: var(--lumo-primary-text-color); + } + `; + + static properties = { + _beans: {state: true}, // <6> + }; + + constructor() { // <7> + super(); + this._beans = beans; + } + + render() { // <8> + if (this._beans) { + return html`
    + ${this._beans.map((bean) => // <9> + html`
  • ${bean.providerType.name}
  • ` + )}
`; + } else { + return html`No beans found`; + } + } +} +customElements.define('qwc-arc-beans', QwcArcBeans); // <10> +---- + +<1> You can import Classes and/or functions from other libraries. +In this case we use the `LitElement` class and `html` & `css` functions from `Lit` +<2> Build time data as defined in the Build step and can be imported using the key and always from `build-time-data`. All keys added in your Build step will be available. +<3> The component should be named in the following format: Qwc (stands for Quarkus Web Component) then Extension Name then Page Title, all concatenated with Camel Case. This will also match the file name format as described earlier. The component should also extend `LitComponent`. +<4> CSS styles can be added using the `css` function, and these styles only apply to your component. +<5> Styles can reference globally defined CSS variables to make sure your page renders correctly, especially when switching between light and dark mode. You can find all CSS variables in the Vaadin documentation (https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/color[Color], https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/size-space[Sizing and Spacing], etc) +<6> Properties can be added. Use `_` in front of a property if that property is private. Properties are usually injected in the HTML template, and can be defined as having state, meaning that if that property changes, the component should re-render. In this case, the beans are Build time data and only change on hot-reload, which will be covered later. +<7> Constructors (optional) should always call `super` first, and then set the default values for the properties. +<8> The render method (from `LitElement`) will be called to render the page. In this method you return the markup of the page you want. You can use the `html` function from `Lit`, that gives you a template language to output the HTML you want. Once the template is created, you only need to set/change the properties to re-render the page content. Read more about https://lit.dev/docs/components/rendering/[Lit html] +<9> You can use the built-in template functions to do conditional, list, etc. Read more about https://lit.dev/docs/templates/overview/[Lit Templates] +<10> You always need to register your Web component as a custom element, with a unique tag. Here the tag will follow the same format as the filename (`qwc` dash `extension name` dash `page title` ); + +===== Using Vaadin UI components for rendering + +Dev UI makes extensive usage of https://vaadin.com/docs/latest/components[Vaadin web components] as UI Building blocks. + +As an example, the Arc Beans are rendered using a https://vaadin.com/docs/latest/components/grid[Vaadin Grid]: + +[source,javascript] +---- +import { LitElement, html, css} from 'lit'; +import { beans } from 'build-time-data'; +import '@vaadin/grid'; // <1> +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; // <2> +import '@vaadin/vertical-layout'; +import 'qui-badge'; // <3> + +/** + * This component shows the Arc Beans + */ +export class QwcArcBeans extends LitElement { + + static styles = css` + .arctable { + height: 100%; + padding-bottom: 10px; + } + + code { + font-size: 85%; + } + + .annotation { + color: var(--lumo-contrast-50pct); + } + + .producer { + color: var(--lumo-primary-text-color); + } + `; + + static properties = { + _beans: {state: true}, + }; + + constructor() { + super(); + this._beans = beans; + } + + render() { + if (this._beans) { + + return html` + + + + + + + + + + `; + + } else { + return html`No beans found`; + } + } + + _beanRenderer(bean) { + return html` + @${bean.scope.simpleName} + ${bean.nonDefaultQualifiers.map(qualifier => + html`${this._qualifierRenderer(qualifier)}` + )} + ${bean.providerType.name} + `; + } + + _kindRenderer(bean) { + return html` + + ${this._kindBadgeRenderer(bean)} + ${this._kindClassRenderer(bean)} + + `; + } + + _kindBadgeRenderer(bean){ + let kind = this._camelize(bean.kind); + let level = null; + + if(bean.kind.toLowerCase() === "field"){ + kind = "Producer field"; + level = "success"; + }else if(bean.kind.toLowerCase() === "method"){ + kind = "Producer method"; + level = "success"; + }else if(bean.kind.toLowerCase() === "synthetic"){ + level = "contrast"; + } + + return html` + ${level + ? html`${kind}` + : html`${kind}` + }`; + } + + _kindClassRenderer(bean){ + return html` + ${bean.declaringClass + ? html`${bean.declaringClass.simpleName}.${bean.memberName}()` + : html`${bean.memberName}` + } + `; + } + + _interceptorsRenderer(bean) { + if (bean.interceptors && bean.interceptors.length > 0) { + return html` + ${bean.interceptorInfos.map(interceptor => + html`
+ ${interceptor.interceptorClass.name} + ${interceptor.priority} +
` + )} +
`; } - {/style} - {#script} // <3> - $(document).ready(function(){ - $(function () { - $('[data-toggle="tooltip"]').tooltip() + } + + _qualifierRenderer(qualifier) { + return html`${qualifier.simpleName}`; + } + + _camelize(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { + if (+match === 0) + return ""; + return index === 0 ? match.toUpperCase() : match.toLowerCase(); }); - }); - {/script} - {#title}Cache{/title}// <4> - {#body}// <5> - - - - - - - - - {#for cacheInfo in info:cacheInfos}// <6> - - - - - {/for} - -
NameSize
- {cacheInfo.name} - -
- enctype="application/x-www-form-urlencoded"> - - - -
-
- {/body} -{/include} ----- -<1> In order to benefit from the same style as other Dev UI pages, extend the `main` template -<2> You can pass extra CSS for your page in the `style` template parameter -<3> You can pass extra JavaScript for your page in the `script` template parameter. This will be added inline after the JQuery script, so you can safely use JQuery in your script. -<4> Don't forget to set your page title in the `title` template parameter -<5> The `body` template parameter will contain your content -<6> In order for your template to read custom information from your Quarkus extension, you can use - the `info` xref:qute-reference.adoc#namespace_extension_methods[namespace]. -<7> This shows an xref:advanced-usage-adding-actions[interactive page] - -== Linking to your full-page templates - -Full-page templates for extensions live under a pre-defined `{devRootAppend}/{groupId}.{artifactId}/` directory -that is referenced using the `urlbase` template parameter. Using configuration defaults, that would resolve to -`/q/dev-v1/io.quarkus.quarkus-cache/`, as an example. - -Use the `{urlbase}` template parameter to reference this folder in `embedded.html`: + } +} +customElements.define('qwc-arc-beans', QwcArcBeans); +---- +<1> Import the Vaadin component you want to use +<2> You can also import other functions if needed +<3> There are some internal UI components that you can use, described below + +===== Using internal UI components + +Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui[internal UI components] (under the `qui` namespace) are available to make certain things easier: + +- Card +- Badge +- Alert +- Code block +- IDE Link + +====== Card + +Card component to display contents in a card + +[source,javascript] +---- +import 'qui-card'; +---- + +[source,html] +---- + +
+ My contents +
+
+---- + +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L110[Example code] + +====== Badge + +Badge UI Component based on the https://vaadin.com/docs/latest/components/badge[vaadin themed] badge + +image::dev-ui-qui-badge-v2.png[alt=Dev UI Badge,role="center"] + +[source,javascript] +---- +import 'qui-badge'; +---- + +You can use any combination of small, primary, pill, with icon and clickable with any level of `default`, `success`, `warning`, `error`, `contrast` or set your own colors. + +[source,html] +---- +
+

Badges

+

Badges wrap the Vaadin theme in a component. + See https://vaadin.com/docs/latest/components/badge for more info. +

+
+ +
+
+ Default + Success + Warning + Error + Contrast + Custom colours +
+
+
+ +
+
+ Default primary + Success primary + Warning primary + Error primary + Contrast primary + Custom colours +
+
+
+ +
+
+ Default pill + Success pill + Warning pill + Error pill + Contrast pill + Custom colours +
+
+
+ +
+
+ + Default icon + + + Success icon + + + Warning icon + + + Error icon + + + Contrast icon + + + Custom colours + +
+
+
+ +
+
+ + + + + + +
+
+
+ +
+
+ this._info()}>Default + this._success()}>Success + this._warning()}>Warning + this._error()}>Error + this._contrast()}>Contrast + this._info()}>Custom colours +
+
+
+
+
+---- + +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L214[Example code] + +====== Alert + +Alerts are modeled around the Bootstrap alerts. Click https://getbootstrap.com/docs/4.0/components/alerts[here] for more info. + +Also see Notification controller below as an alternative. + +image::dev-ui-qui-alert-v2.png[alt=Dev UI Alert,role="center"] + +[source,javascript] +---- +import 'qui-alert'; +---- + +[source,html] +---- +
+
+ Info alert + Success alert + Warning alert + Error alert +
+ Permanent Info alert + Permanent Success alert + Permanent Warning alert + Permanent Error alert +
+ Center Info alert + Center Success alert + Center Warning alert + Center Error alert +
+ Info alert with icon + Success alert with icon + Warning alert with icon + Error alert with icon +
+ Info alert with custom icon + Success alert with custom icon + Warning alert with custom icon + Error alert with custom icon +
+ Small Info alert with icon + Small Success alert with icon + Small Warning alert with icon + Small Error alert with icon +
+ Info alert with markup
quarkus.io
+ Success alert with markup
quarkus.io
+ Warning alert with markup
quarkus.io
+ Error alert with markup
quarkus.io
+
+ Primary Info alert with icon + Primary Success alert with icon + Primary Warning alert with icon + Primary Error alert with icon +
+ Info alert with title + Success alert with title + Warning alert with title + Error alert with title +
+ Info alert with title and icon + Success alert with title and icon + Warning alert with title and icon + Error alert with title and icon +
+
+---- + +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L316[Example code] + + +====== Code block + +Display a code block. This component is aware of the theme and will use the correct codemirror theme to match the light/dark mode. + + +image::dev-ui-qui-code-block-v2.png[alt=Dev UI Code Block,role="center"] + +[source,javascript] +---- +import 'qui-code-block'; +---- + +[source,html] +---- +
+ + +
; +---- + +https://github.com/quarkusio/quarkus/blob/e03a97845738436c69443a591ec4ce88ed04ac91/extensions/kubernetes/vanilla/deployment/src/main/resources/dev-ui/qwc-kubernetes-manifest.js#L99[Example code] + +or fetching the contents from a URL: + +[source,html] +---- +
+ + +
+---- + +https://github.com/quarkusio/quarkus/blob/95c54fa46a6b6f31d69477234486d9359a2a3a4a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js#L116[Example code] + +====== IDE link + +Creates a link to a resource (like a Java source file) that can be opened in the user's IDE (if we could detect the IDE). + +[source,javascript] +---- +import 'qui-ide-link'; +---- + +[source,html] +---- +[${sourceClassNameFull}]; +---- + +https://github.com/quarkusio/quarkus/blob/582f1f78806d2268885faea7aa8f5a4d2b3f5b98/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js#L315[Example code] + +===== Using internal controllers + +Some https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller[internal controllers] are available to make certain things easier: + +- Notifier +- Storage +- Log +- Router +- JsonRPC + +====== Notifier + +This is an easy way to show a toast message. The toast can be placed on the screen (default left bottom) and can have a level (Info, Success, Warning, Error). Any of the levels can also be primary, that will create a more prominent toast message. + +See the source of this controller https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js[here]. + +Example usage: + +image::dev-ui-controller-notifier.gif[alt=Dev UI Notifier,role="center"] + +[source,javascript] +---- +import { notifier } from 'notifier'; +---- [source,html] ---- -// <1> - - Caches {info:cacheInfos.size()} + this._info()}>Info; +---- + +[source,javascript] +---- +_info(position = null){ + notifier.showInfoMessage("This is an information message", position); +} +---- + +You can find all the valid positions https://vaadin.com/docs/latest/components/notification/#position[here]. + +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/resources/dev-ui/qwc-jokes-vaadin.js#L374[Example code] + +====== Storage + +An easy way to access the local storage in a safe way. This will store values in the local storage, scoped to your extension. This way you do not have to worry that you might clash with another extension. + +Local storage is useful to remember user preferences or state. For example, the footer remembers the state (open/close) and the size when open of the bottom drawer. + +[source,javascript] +---- +import { StorageController } from 'storage-controller'; + +// ... + +storageControl = new StorageController(this); // Passing in this will scope the storage to your extension + +// ... + +const storedHeight = this.storageControl.get("height"); // Get some value + +// ... + +this.storageControl.set('height', 123); // Set some val +---- + +https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js[Example code] + + +====== Log + +The log controller is used to add control buttons to a (footer) log. +See <>. + +image::dev-ui-log-control-v2.png[alt=Dev UI Log control,role="center"] + +[source,javascript] ---- -<1> Use the `urlbase` template parameter to reference full-page templates for your extension +import { LogController } from 'log-controller'; + +// ... + +logControl = new LogController(this); // Passing in this will scope the control to your extension + +// ... +this.logControl + .addToggle("On/off switch", true, (e) => { + this._toggleOnOffClicked(e); + }).addItem("Log levels", "font-awesome-solid:layer-group", "var(--lumo-tertiary-text-color)", (e) => { + this._logLevels(); + }).addItem("Columns", "font-awesome-solid:table-columns", "var(--lumo-tertiary-text-color)", (e) => { + this._columns(); + }).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "var(--lumo-tertiary-text-color)", (e) => { + this._zoomOut(); + }).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "var(--lumo-tertiary-text-color)", (e) => { + this._zoomIn(); + }).addItem("Clear", "font-awesome-solid:trash-can", "var(--lumo-error-color)", (e) => { + this._clearLog(); + }).addFollow("Follow log", true , (e) => { + this._toggleFollowLog(e); + }).done(); +---- + +https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js[Example code] + +====== Router + +The router is mostly used internally. This uses https://github.com/vaadin/router[Vaadin Router] under the covers to route URLs to the correct page/section within the SPA. It will update the navigation and allow history (back button). This also creates the sub-menu available on extensions that have multiple pages. + +See the https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js[controller] for some methods that might be useful. -== Passing information to your templates +[#JsonRPC] +====== JsonRPC -In `embedded.html` or in full-page templates, you will likely want to display information that is -available from your extension. +This controller allows you to fetch or stream runtime data. (vs. <> discussed earlier). There are two parts to getting data during runtime. The Java side in the runtime module, and then the usage in the web component. -There are two ways to make that information available, depending on whether it is available at -build time or at run time. +*Java part* -In both cases we advise that you add support for the Dev UI in your `{pkg}.deployment.devconsole` -package in a `DevConsoleProcessor` class (in your extension's -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`deployment`] module). +This code is responsible for making data available to display on the UI. -=== Passing run-time information +You need to register the JsonPRCService in your processor in the deployment module: [source,java] ---- -package io.quarkus.cache.deployment.devconsole; +@BuildStep(onlyIf = IsDevelopment.class)// <1> +JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {// <2> + return new JsonRPCProvidersBuildItem(CacheJsonRPCService.class);// <3> +} +---- +<1> Always only do this in Dev Mode +<2> Produce / return a `JsonRPCProvidersBuildItem` +<3> Define the class in your runtime module that will contain methods that make data available in the UI + +https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/devui/CacheDevUiProcessor.java[Example code] + +Now, in your Runtime module create the JsonRPC Service. This class will default to an application scoped bean, except if you explicitly scope the bean. All public methods that return something will be made available to call from the Web component Javascript. + +The return object in these methods can be: -import io.quarkus.cache.runtime.CaffeineCacheSupplier; -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; +- primitives or `String`, +- `io.vertx.core.json.JsonArray` +- `io.vertx.core.json.JsonObject` +- any other POJO that can be serializable to Json -public class DevConsoleProcessor { +All of the above can be blocking (POJO) or non-blocking (`@NonBlocking` or `Uni`). Or alternatively data can be streamed using `Multi`. - @BuildStep(onlyIf = IsDevelopment.class)// <1> - public DevConsoleRuntimeTemplateInfoBuildItem collectBeanInfo() { - return new DevConsoleRuntimeTemplateInfoBuildItem("cacheInfos", - new CaffeineCacheSupplier());// <2> +[source,java] +---- +@NonBlocking // <1> +public JsonArray getAll() { // <2> + Collection names = manager.getCacheNames(); + List allCaches = new ArrayList<>(names.size()); + for (String name : names) { + Optional cache = manager.getCache(name); + if (cache.isPresent() && cache.get() instanceof CaffeineCache) { + allCaches.add((CaffeineCache) cache.get()); + } } + allCaches.sort(Comparator.comparing(CaffeineCache::getName)); + + var array = new JsonArray(); + for (CaffeineCache cc : allCaches) { + array.add(getJsonRepresentationForCache(cc)); + } + return array; } ---- -<1> Don't forget to make this xref:building-my-first-extension.adoc#deploying-the-greeting-feature[build step] - conditional on being in dev mode -<2> Declare a run-time dev `info:cacheInfos` template value +<1> This example runs non blocking. We could also return `Uni` +<2> The method name `getAll` will be available in the Javascript + +https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devconsole/CacheJsonRPCService.java[Example code] + +*Webcomponent (Javascript) part* + +Now you can use the JsonRPC controller to access the `getAll` method (and any other methods in you JsonRPC Service) + +[source,javascript] +---- +import { JsonRpc } from 'jsonrpc'; + +// ... + +jsonRpc = new JsonRpc(this); // Passing in this will scope the rpc calls to your extension + +// ... + +/** + * Called when displayed + */ +connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getAll().then(jsonRpcResponse => { // <1> + this._caches = new Map(); + jsonRpcResponse.result.forEach(c => { //<2> + this._caches.set(c.name, c); + }); + }); +} +---- + +<1> Note the method `getAll` corresponds to the method in your Java Service. This method returns a https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise[Promise] with the JsonRPC result. +<2> In this case the result is an array, so we can loop over it. + +JsonArray (or any Java collection) in either blocking or non-blocking will return an array, else a JsonObject will be returned. + +https://github.com/quarkusio/quarkus/blob/main/extensions/cache/deployment/src/main/resources/dev-ui/qwc-cache-caches.js[Example code] -This will map the `info:cacheInfos` value to this supplier in your extension's -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`runtime module`]: +You can also pass in parameters in the method being called, for example: +(In the Runtime Java code) [source,java] ---- -package io.quarkus.cache.runtime; +public Uni clear(String name) { //<1> + Optional cache = manager.getCache(name); + if (cache.isPresent()) { + return cache.get().invalidateAll().map((t) -> getJsonRepresentationForCache(cache.get())); + } else { + return Uni.createFrom().item(new JsonObject().put("name", name).put("size", -1)); + } +} +---- +<1> the clear method takes one parameter called `name` + +In the Webcomponent (Javascript): + +[source,javascript] +---- +_clear(name) { + this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { //<1> + this._updateCache(jsonRpcResponse.result) + }); +} +---- +<1> the `name` parameter is passed in. -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.function.Supplier; +====== Streaming data -import io.quarkus.arc.Arc; -import io.quarkus.cache.CaffeineCache; +You can keep a UI screen updated with the latest data by continuously streaming data to the screen. This can be done with `Multi` (Java side) and `Observer` (Javascript side) -public class CaffeineCacheSupplier implements Supplier> { +Java side of streaming data: - @Override - public List get() { - List allCaches = new ArrayList<>(allCaches()); - allCaches.sort(Comparator.comparing(CaffeineCache::getName)); - return allCaches; +[source,java] +---- +public class JokesJsonRPCService { + + private final BroadcastProcessor jokeStream = BroadcastProcessor.create(); + + @PostConstruct + void init() { + Multi.createFrom().ticks().every(Duration.ofHours(4)).subscribe().with((item) -> { + jokeStream.onNext(getJoke()); + }); } - public static Collection allCaches() { - // Get it from ArC at run-time - return (Collection) (Collection) - Arc.container().instance(CacheManagerImpl.class).get().getAllCaches(); + public Multi streamJokes() { // <1> + return jokeStream; } + + // ... } ---- +<1> Return the Multi that will stream jokes -=== Passing build-time information +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/runtime/src/main/java/io/quarkus/jokes/runtime/JokesJsonRPCService.java#L37[Example code] -Sometimes you only need build-time information to be passed to your template, so you can do it like this: +Javascript side of streaming data: -[source,java] +[source,javascript] ---- -package io.quarkus.qute.deployment.devconsole; - -import java.util.List; - -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; -import io.quarkus.qute.deployment.CheckedTemplateBuildItem; -import io.quarkus.qute.deployment.TemplateVariantsBuildItem; - -public class DevConsoleProcessor { - - @BuildStep(onlyIf = IsDevelopment.class) - public DevConsoleTemplateInfoBuildItem collectBeanInfo( - List checkedTemplates,// <1> - TemplateVariantsBuildItem variants) { - DevQuteInfos quteInfos = new DevQuteInfos(); - for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { - DevQuteTemplateInfo templateInfo = - new DevQuteTemplateInfo(checkedTemplate.templateId, - variants.getVariants().get(checkedTemplate.templateId), - checkedTemplate.bindings); - quteInfos.addQuteTemplateInfo(templateInfo); - } - return new DevConsoleTemplateInfoBuildItem("devQuteInfos", quteInfos);// <2> +this._observer = this.jsonRpc.streamJokes().onNext(jsonRpcResponse => { //<1> + this._addToJokes(jsonRpcResponse.result); + this._numberOfJokes = this._numberOfJokes++; +}); + +// ... + +this._observer.cancel(); //<2> +---- +<1> You can call the method (optionally passing in parameters) and then provide the code that will be called on the next event. +<2> Make sure to keep an instance of the observer to cancel later if needed. + +https://github.com/phillip-kruger/quarkus-jokes/blob/main/deployment/src/main/resources/dev-ui/qwc-jokes-web-components.js[Example code] + +====== Dev UI Log + +When running a local application using the `999-SNAPSHOT` version, the Dev UI will show a `Dev UI` Log in the footer. This is useful to debug all JSON RPC messages flowing between the browser and the Quarkus app. + +image::dev-ui-jsonrpc-log-v2.png[alt=Dev UI Json RPC Log,role="center"] + +== Hot reload + +You can update a screen automatically when a Hot reload has happened. To do this replace the `LitElement` that your Webcomponent extends with `QwcHotReloadElement`. + +`QwcHotReloadElement` extends `LitElement` so your component is still a Lit Element. + +When extending a `QwcHotReloadElement` you have to provide the `hotReload` method. (You also still need to provide the `render` method from Lit) + +[source,javascript] +---- +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; + +// ... + +export class QwcMyExtensionPage extends QwcHotReloadElement { + + render(){ + // ... } + hotReload(){ + // .. + } + +} +---- + +== Custom cards + +You can customize the card that is being displayed on the extension page if you do not want to use the default built-in card. + +To do this, you need to provide a Webcomponent that will be loaded in the place of the provided card and register this in the Java Processor: + +[source,java] +---- +cardPageBuildItem.setCustomCard("qwc-mycustom-card.js"); +---- + +On the Javascript side, you have access to all the pages (in case you want to create links) + +[source,javascript] +---- +import { pages } from 'build-time-data'; +---- + +And the following properties will be passed in: + +- extensionName +- description +- guide +- namespace + +[source,javascript] +---- +static properties = { + extensionName: {type: String}, + description: {type: String}, + guide: {type: String}, + namespace: {type: String} } ---- -<1> Use whatever dependencies you need as input -<2> Declare a build-time `info:devQuteInfos` DEV template value -== Advanced usage: adding actions +== State (Advance) + +State allows properties to contain state and can be reused globally. An example of state properties are the theme, the connection state (if we are connected to the backend), etc. + +See the https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state[current built-in] state objects. -You can also add actions to your Dev UI templates: +The state in Dev UI uses https://github.com/gitaarik/lit-state[LitState] and you can read more about it in their https://gitaarik.github.io/lit-state/build/[documentation]. -image::dev-ui-interactive.png[alt=Dev UI interactive page,align=center,width=90%] -This can be done by adding another xref:building-my-first-extension.adoc#deploying-the-greeting-feature[build step] to -declare the action in your extension's -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`deployment`] module: +== Add a log file +Apart from adding a card and a page, extensions can add a log to the footer. This is useful to log things happening continuously. A page will lose connection to the backend when navigating away from that page, a log in the footer is permanently connected. + +Adding something to the footer works exactly like adding a Card, except you use a `FooterPageBuildItem` rather than a `CardPageBuildItem`. [source,java] ---- -package io.quarkus.cache.deployment.devconsole; +FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem(); -import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +footerPageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-regular:face-grin-tongue-wink") + .title("Joke Log") + .componentLink("qwc-jokes-log.js")); -import io.quarkus.cache.runtime.devconsole.CacheDevConsoleRecorder; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +footerProducer.produce(footerPageBuildItem); +---- -public class DevConsoleProcessor { +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/java/io/quarkus/jokes/deployment/devui/JokesDevUIProcessor.java#L87[Example code] - @BuildStep - @Record(value = STATIC_INIT, optional = true)// <1> - DevConsoleRouteBuildItem invokeEndpoint(CacheDevConsoleRecorder recorder) { - return new DevConsoleRouteBuildItem("caches", "POST", - recorder.clearCacheHandler());// <2> - } +In your Webcomponent you can then stream the log to the UI: + +[source,javascript] +---- +export class QwcJokesLog extends LitElement { + jsonRpc = new JsonRpc(this); + logControl = new LogController(this); + + // .... } ---- -<1> Mark the recorder as optional, so it will only be invoked when in dev mode -<2> Declare a `POST {urlbase}/caches` route handled by the given handler +https://github.com/phillip-kruger/quarkus-jokes/blob/main/deployment/src/main/resources/dev-ui/qwc-jokes-log.js[Example code] -Note: you can see xref:action-example[how to invoke this action from your full page]. +== Add a section menu -Now all you have to do is implement the recorder in your extension's -xref:building-my-first-extension.adoc#description-of-a-quarkus-extension[`runtime module`]: +This allows an extension to link a page directly in the section Menu. +Adding something to the section menu works exactly like adding a Card, except you use a `MenuPageBuildItem` rather than a `CardPageBuildItem`. [source,java] ---- -package io.quarkus.cache.runtime.devconsole; - -import io.quarkus.cache.CaffeineCache; -import io.quarkus.cache.runtime.CaffeineCacheSupplier; -import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; -import io.quarkus.vertx.http.runtime.devmode.devconsole.FlashScopeUtil.FlashMessageStatus; -import io.vertx.core.Handler; -import io.vertx.core.MultiMap; -import io.vertx.ext.web.RoutingContext; - -@Recorder -public class CacheDevConsoleRecorder { - - public Handler clearCacheHandler() { - return new DevConsolePostHandler() {// <1> - @Override - protected void handlePost(RoutingContext event, MultiMap form) // <2> - throws Exception { - String cacheName = form.get("name"); - for (CaffeineCache cache : CaffeineCacheSupplier.allCaches()) { - if (cache.getName().equals(cacheName)) { - cache.invalidateAll(); - flashMessage(event, "Cache for " + cacheName + " cleared");// <3> - return; - } - } - flashMessage(event, "Cache for " + cacheName + " not found", - FlashMessageStatus.ERROR);// <4> - } - }; +MenuPageBuildItem menuPageBuildItem = new MenuPageBuildItem(); + +menuPageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-regular:face-grin-tongue-wink") + .title("One Joke") + .componentLink("qwc-jokes-menu.js")); + +menuProducer.produce(menuPageBuildItem); +---- + +https://github.com/phillip-kruger/quarkus-jokes/blob/f572ed6f949de0c0b8cbfa99d7389ab5168fea65/deployment/src/main/java/io/quarkus/jokes/deployment/devui/JokesDevUIProcessor.java#LL71C16-L71C16[Example code] + +Your page can be any Page similar to Cards. + +== Testing + +You can add tests to your extension that test: + +- Build time data +- Runtime data via JsonRPC + +You need to add this to your pom: + +[source,xml] +---- + + io.quarkus + quarkus-vertx-http-dev-ui-tests + test + +---- + +This will give you access to two base classes for creating these tests. + +=== Testing Build time data + +If you added Build time data, for example: + +[source,java] +---- +cardPageBuildItem.addBuildTimeData("somekey", somevalue); +---- + +To test that your build time data is generated correctly you can add a test that extends `DevUIBuildTimeDataTest`. + +[source,java] +---- +public class SomeTest extends DevUIBuildTimeDataTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication(); + + public SomeTest() { + super("io.quarkus.my-extension"); } + + @Test + public void testSomekey() throws Exception { + JsonNode somekeyResponse = super.getBuildTimeData("somekey"); + Assertions.assertNotNull(somekeyResponse); + + // Check more values on somekeyResponse + } + +} +---- + +=== Testing Runtime data + +If you added a JsonRPC Service with runtime data responses, for example: + +[source,java] +---- +public boolean updateProperties(String content, String type) { + // ... } ---- -<1> While you can use https://vertx.io/docs/vertx-web/java/#_routing_by_http_method[any Vert.x handler], - the `DevConsolePostHandler` superclass will handle your POST actions - nicely, and auto-redirect to the `GET` URI right after your `POST` for optimal behavior. -<2> You can get the Vert.x `RoutingContext` as well as the `form` contents -<3> Don't forget to add a message for the user to let them know everything went fine -<4> You can also add error messages +To test that `updateProperties` execute correctly via JsonRPC you can add a test that extends `DevUIJsonRPCTest`. + +[source,java] +---- +public class SomeTest extends DevUIJsonRPCTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication(); -NOTE: Flash messages are handled by the `main` DEV template and will result in nice notifications for your -users: + public SomeTest() { + super("io.quarkus.my-extension"); + } + + @Test + public void testUpdateProperties() throws Exception { -image::dev-ui-message.png[alt=Dev UI message,align=center,width=90%] + JsonNode updatePropertyResponse = super.executeJsonRPCMethod("updateProperty", + Map.of( + "name", "quarkus.application.name", + "value", "changedByTest")); + Assertions.assertTrue(updatePropertyResponse.asBoolean()); + // Get the properties to make sure it is changed + JsonNode allPropertiesResponse = super.executeJsonRPCMethod("getAllValues"); + String applicationName = allPropertiesResponse.get("quarkus.application.name").asText(); + Assertions.assertEquals("changedByTest", applicationName); + } +} +---- diff --git a/_versions/main/guides/optaplanner.adoc b/_versions/main/guides/optaplanner.adoc deleted file mode 100644 index 07bdf01fba..0000000000 --- a/_versions/main/guides/optaplanner.adoc +++ /dev/null @@ -1,1100 +0,0 @@ -//// -This guide is maintained in the main Quarkus repository -and pull requests should be submitted there: -https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc -//// -= OptaPlanner - Using AI to optimize a schedule with OptaPlanner -include::_attributes.adoc[] -:categories: business-automation -:summary: This guide walks you through the process of creating a Quarkus application with OptaPlanner's constraint solving Artificial Intelligence (AI). -:config-file: application.properties - -This guide walks you through the process of creating a Quarkus application -with https://www.optaplanner.org/[OptaPlanner]'s constraint solving Artificial Intelligence (AI). - -== What you will build - -You will build a REST application that optimizes a school timetable for students and teachers: - -image::optaplanner-time-table-app-screenshot.png[] - -Your service will assign `Lesson` instances to `Timeslot` and `Room` instances automatically -by using AI to adhere to hard and soft scheduling _constraints_, such as the following examples: - -* A room can have at most one lesson at the same time. -* A teacher can teach at most one lesson at the same time. -* A student can attend at most one lesson at the same time. -* A teacher prefers to teach all lessons in the same room. -* A teacher prefers to teach sequential lessons and dislikes gaps between lessons. -* A student dislikes sequential lessons on the same subject. - -Mathematically speaking, school timetabling is an _NP-hard_ problem. -This means it is difficult to scale. -Simply brute force iterating through all possible combinations takes millions of years -for a non-trivial dataset, even on a supercomputer. -Luckily, AI constraint solvers such as OptaPlanner have advanced algorithms -that deliver a near-optimal solution in a reasonable amount of time. - -[[solution]] -== Solution - -We recommend that you follow the instructions in the next sections and create the application step by step. -However, you can go right to the completed example. - -Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. - -The solution is located in link:{quickstarts-tree-url}/optaplanner-quickstart[the `optaplanner-quickstart` directory]. - -== Prerequisites - -:prerequisites-time: 30 minutes -:prerequisites-no-graalvm: -include::{includes}/prerequisites.adoc[] - -== The build file and the dependencies - -Use https://code.quarkus.io/[code.quarkus.io] to generate an application -with the following extensions, for Maven or Gradle: - -* RESTEasy Reactive (`quarkus-resteasy-reactive`) -* RESTEasy Reactive Jackson (`quarkus-resteasy-reactive-jackson`) -* OptaPlanner (`optaplanner-quarkus`) -* OptaPlanner Jackson (`optaplanner-quarkus-jackson`) - -Alternatively, generate it from the command line: - -:create-app-artifact-id: optaplanner-quickstart -:create-app-extensions: resteasy-reactive,resteasy-reactive-jackson,optaplanner-quarkus,optaplanner-quarkus-jackson -include::{includes}/devtools/create-app.adoc[] - -This will include the following dependencies in your build file: - -[source,xml,subs=attributes+,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - - - {quarkus-platform-groupid} - quarkus-bom - {quarkus-version} - pom - import - - - {quarkus-platform-groupid} - quarkus-optaplanner-bom - {quarkus-version} - pom - import - - - - - - io.quarkus - quarkus-resteasy-reactive - - - io.quarkus - quarkus-resteasy-reactive-jackson - - - org.optaplanner - optaplanner-quarkus - - - org.optaplanner - optaplanner-quarkus-jackson - - - - io.quarkus - quarkus-junit5 - test - - ----- - -[source,gradle,subs=attributes+,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -dependencies { - implementation enforcedPlatform("{quarkus-platform-groupid}:quarkus-bom:{quarkus-version}") - implementation enforcedPlatform("{quarkus-platform-groupid}:quarkus-optaplanner-bom:{quarkus-version}") - implementation 'io.quarkus:quarkus-resteasy-reactive' - implementation 'io.quarkus:quarkus-resteasy-reactive-jackson' - implementation 'org.optaplanner:optaplanner-quarkus' - implementation 'org.optaplanner:optaplanner-quarkus-jackson' - - testImplementation 'io.quarkus:quarkus-junit5' -} ----- - -== Model the domain objects - -Your goal is to assign each lesson to a time slot and a room. -You will create these classes: - -image::optaplanner-time-table-class-diagram-pure.png[] - -=== Timeslot - -The `Timeslot` class represents a time interval when lessons are taught, -for example, `Monday 10:30 - 11:30` or `Tuesday 13:30 - 14:30`. -For simplicity's sake, all time slots have the same duration -and there are no time slots during lunch or other breaks. - -A time slot has no date, because a high school schedule just repeats every week. -So there is no need for https://docs.optaplanner.org/latestFinal/optaplanner-docs/html_single/index.html#continuousPlanning[continuous planning]. - -Create the `src/main/java/org/acme/optaplanner/domain/Timeslot.java` class: - -[source,java] ----- -package org.acme.optaplanner.domain; - -import java.time.DayOfWeek; -import java.time.LocalTime; - -public class Timeslot { - - private DayOfWeek dayOfWeek; - private LocalTime startTime; - private LocalTime endTime; - - public Timeslot() { - } - - public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { - this.dayOfWeek = dayOfWeek; - this.startTime = startTime; - this.endTime = endTime; - } - - public DayOfWeek getDayOfWeek() { - return dayOfWeek; - } - - public LocalTime getStartTime() { - return startTime; - } - - public LocalTime getEndTime() { - return endTime; - } - - @Override - public String toString() { - return dayOfWeek + " " + startTime; - } - -} ----- - -Because no `Timeslot` instances change during solving, a `Timeslot` is called a _problem fact_. -Such classes do not require any OptaPlanner specific annotations. - -Notice the `toString()` method keeps the output short, -so it is easier to read OptaPlanner's `DEBUG` or `TRACE` log, as shown later. - -=== Room - -The `Room` class represents a location where lessons are taught, -for example, `Room A` or `Room B`. -For simplicity's sake, all rooms are without capacity limits -and they can accommodate all lessons. - -Create the `src/main/java/org/acme/optaplanner/domain/Room.java` class: - -[source,java] ----- -package org.acme.optaplanner.domain; - -public class Room { - - private String name; - - public Room() { - } - - public Room(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - -} ----- - -`Room` instances do not change during solving, so `Room` is also a _problem fact_. - -=== Lesson - -During a lesson, represented by the `Lesson` class, -a teacher teaches a subject to a group of students, -for example, `Math by A.Turing for 9th grade` or `Chemistry by M.Curie for 10th grade`. -If a subject is taught multiple times per week by the same teacher to the same student group, -there are multiple `Lesson` instances that are only distinguishable by `id`. -For example, the 9th grade has six math lessons a week. - -During solving, OptaPlanner changes the `timeslot` and `room` fields of the `Lesson` class, -to assign each lesson to a time slot and a room. -Because OptaPlanner changes these fields, `Lesson` is a _planning entity_: - -image::optaplanner-time-table-class-diagram-annotated.png[] - -Most of the fields in the previous diagram contain input data, except for the orange fields: -A lesson's `timeslot` and `room` fields are unassigned (`null`) in the input data -and assigned (not `null`) in the output data. -OptaPlanner changes these fields during solving. -Such fields are called planning variables. -In order for OptaPlanner to recognize them, -both the `timeslot` and `room` fields require an `@PlanningVariable` annotation. -Their containing class, `Lesson`, requires an `@PlanningEntity` annotation. - -Create the `src/main/java/org/acme/optaplanner/domain/Lesson.java` class: - -[source,java] ----- -package org.acme.optaplanner.domain; - -import org.optaplanner.core.api.domain.entity.PlanningEntity; -import org.optaplanner.core.api.domain.lookup.PlanningId; -import org.optaplanner.core.api.domain.variable.PlanningVariable; - -@PlanningEntity -public class Lesson { - - @PlanningId - private Long id; - - private String subject; - private String teacher; - private String studentGroup; - - @PlanningVariable - private Timeslot timeslot; - @PlanningVariable - private Room room; - - public Lesson() { - } - - public Lesson(Long id, String subject, String teacher, String studentGroup) { - this.id = id; - this.subject = subject; - this.teacher = teacher; - this.studentGroup = studentGroup; - } - - public Long getId() { - return id; - } - - public String getSubject() { - return subject; - } - - public String getTeacher() { - return teacher; - } - - public String getStudentGroup() { - return studentGroup; - } - - public Timeslot getTimeslot() { - return timeslot; - } - - public void setTimeslot(Timeslot timeslot) { - this.timeslot = timeslot; - } - - public Room getRoom() { - return room; - } - - public void setRoom(Room room) { - this.room = room; - } - - @Override - public String toString() { - return subject + "(" + id + ")"; - } - -} ----- - -The `Lesson` class has an `@PlanningEntity` annotation, -so OptaPlanner knows that this class changes during solving -because it contains one or more planning variables. - -The `timeslot` field has an `@PlanningVariable` annotation, so OptaPlanner knows that it can change its value. -In order to find potential Timeslot instances to assign to this field, OptaPlanner uses the variable type to connect to a value range provider that provides a List to pick from. - -The `room` field also has an `@PlanningVariable` annotation, for the same reasons. - -[NOTE] -==== -Determining the `@PlanningVariable` fields for an arbitrary constraint solving use case -is often challenging the first time. -Read https://docs.optaplanner.org/latestFinal/optaplanner-docs/html_single/index.html#domainModelingGuide[the domain modeling guidelines] -to avoid common pitfalls. -==== - -== Define the constraints and calculate the score - -A _score_ represents the quality of a specific solution. -The higher, the better. -OptaPlanner looks for the best solution, which is the solution with the highest score found in the available time. -It might be the _optimal_ solution. - -Because this use case has hard and soft constraints, -use the `HardSoftScore` class to represent the score: - -* Hard constraints must not be broken. For example: _A room can have at most one lesson at the same time._ -* Soft constraints should not be broken. For example: _A teacher prefers to teach in a single room._ - -Hard constraints are weighted against other hard constraints. -Soft constraints are weighted too, against other soft constraints. -*Hard constraints always outweigh soft constraints*, regardless of their respective weights. - -To calculate the score, you could implement an `EasyScoreCalculator` class: - -[source,java] ----- -public class TimeTableEasyScoreCalculator implements EasyScoreCalculator { - - @Override - public HardSoftScore calculateScore(TimeTable timeTable) { - List lessonList = timeTable.getLessonList(); - int hardScore = 0; - for (Lesson a : lessonList) { - for (Lesson b : lessonList) { - if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot()) - && a.getId() < b.getId()) { - // A room can accommodate at most one lesson at the same time. - if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) { - hardScore--; - } - // A teacher can teach at most one lesson at the same time. - if (a.getTeacher().equals(b.getTeacher())) { - hardScore--; - } - // A student can attend at most one lesson at the same time. - if (a.getStudentGroup().equals(b.getStudentGroup())) { - hardScore--; - } - } - } - } - int softScore = 0; - // Soft constraints are only implemented in the optaplanner-quickstarts code - return HardSoftScore.of(hardScore, softScore); - } - -} ----- - -Unfortunately **that does not scale well**, because it is non-incremental: -every time a lesson is assigned to a different time slot or room, -all lessons are re-evaluated to calculate the new score. - -Instead, create a `src/main/java/org/acme/optaplanner/solver/TimeTableConstraintProvider.java` class -to perform incremental score calculation. -It uses OptaPlanner's ConstraintStream API which is inspired by Java Streams and SQL: - -[source,java] ----- -package org.acme.optaplanner.solver; - -import org.acme.optaplanner.domain.Lesson; -import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; -import org.optaplanner.core.api.score.stream.Constraint; -import org.optaplanner.core.api.score.stream.ConstraintFactory; -import org.optaplanner.core.api.score.stream.ConstraintProvider; -import org.optaplanner.core.api.score.stream.Joiners; - -public class TimeTableConstraintProvider implements ConstraintProvider { - - @Override - public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { - // Hard constraints - roomConflict(constraintFactory), - teacherConflict(constraintFactory), - studentGroupConflict(constraintFactory), - // Soft constraints are only implemented in the optaplanner-quickstarts code - }; - } - - Constraint roomConflict(ConstraintFactory constraintFactory) { - // A room can accommodate at most one lesson at the same time. - - // Select a lesson ... - return constraintFactory - .forEach(Lesson.class) - // ... and pair it with another lesson ... - .join(Lesson.class, - // ... in the same timeslot ... - Joiners.equal(Lesson::getTimeslot), - // ... in the same room ... - Joiners.equal(Lesson::getRoom), - // ... and the pair is unique (different id, no reverse pairs) ... - Joiners.lessThan(Lesson::getId)) - // ... then penalize each pair with a hard weight. - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Room conflict"); - } - - Constraint teacherConflict(ConstraintFactory constraintFactory) { - // A teacher can teach at most one lesson at the same time. - return constraintFactory.forEach(Lesson.class) - .join(Lesson.class, - Joiners.equal(Lesson::getTimeslot), - Joiners.equal(Lesson::getTeacher), - Joiners.lessThan(Lesson::getId)) - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Teacher conflict"); - } - - Constraint studentGroupConflict(ConstraintFactory constraintFactory) { - // A student can attend at most one lesson at the same time. - return constraintFactory.forEach(Lesson.class) - .join(Lesson.class, - Joiners.equal(Lesson::getTimeslot), - Joiners.equal(Lesson::getStudentGroup), - Joiners.lessThan(Lesson::getId)) - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Student group conflict"); - } - -} ----- - -The `ConstraintProvider` scales an order of magnitude better than the `EasyScoreCalculator`: __O__(n) instead of __O__(n²). - -== Gather the domain objects in a planning solution - -A `TimeTable` wraps all `Timeslot`, `Room`, and `Lesson` instances of a single dataset. -Furthermore, because it contains all lessons, each with a specific planning variable state, -it is a _planning solution_ and it has a score: - -* If lessons are still unassigned, then it is an _uninitialized_ solution, -for example, a solution with the score `-4init/0hard/0soft`. -* If it breaks hard constraints, then it is an _infeasible_ solution, -for example, a solution with the score `-2hard/-3soft`. -* If it adheres to all hard constraints, then it is a _feasible_ solution, -for example, a solution with the score `0hard/-7soft`. - -Create the `src/main/java/org/acme/optaplanner/domain/TimeTable.java` class: - -[source,java] ----- -package org.acme.optaplanner.domain; - -import java.util.List; - -import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; -import org.optaplanner.core.api.domain.solution.PlanningScore; -import org.optaplanner.core.api.domain.solution.PlanningSolution; -import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty; -import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; -import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; - -@PlanningSolution -public class TimeTable { - - @ValueRangeProvider - @ProblemFactCollectionProperty - private List timeslotList; - @ValueRangeProvider - @ProblemFactCollectionProperty - private List roomList; - @PlanningEntityCollectionProperty - private List lessonList; - - @PlanningScore - private HardSoftScore score; - - public TimeTable() { - } - - public TimeTable(List timeslotList, List roomList, List lessonList) { - this.timeslotList = timeslotList; - this.roomList = roomList; - this.lessonList = lessonList; - } - - public List getTimeslotList() { - return timeslotList; - } - - public List getRoomList() { - return roomList; - } - - public List getLessonList() { - return lessonList; - } - - public HardSoftScore getScore() { - return score; - } - -} ----- - -The `TimeTable` class has an `@PlanningSolution` annotation, -so OptaPlanner knows that this class contains all the input and output data. - -Specifically, this class is the input of the problem: - -* A `timeslotList` field with all time slots -** This is a list of problem facts, because they do not change during solving. -* A `roomList` field with all rooms -** This is a list of problem facts, because they do not change during solving. -* A `lessonList` field with all lessons -** This is a list of planning entities, because they change during solving. -** Of each `Lesson`: -*** The values of the `timeslot` and `room` fields are typically still `null`, so unassigned. -They are planning variables. -*** The other fields, such as `subject`, `teacher` and `studentGroup`, are filled in. -These fields are problem properties. - -However, this class is also the output of the solution: - -* A `lessonList` field for which each `Lesson` instance has non-null `timeslot` and `room` fields after solving -* A `score` field that represents the quality of the output solution, for example, `0hard/-5soft` - -=== The value range providers - -The `timeslotList` field is a value range provider. -It holds the `Timeslot` instances which OptaPlanner can pick from to assign to the `timeslot` field of `Lesson` instances. -The `timeslotList` field has an `@ValueRangeProvider` annotation to connect the `@PlanningVariable` with the `@ValueRangeProvider`, -by matching the type of the planning variable with the type returned by the value range provider. - -Following the same logic, the `roomList` field also has an `@ValueRangeProvider` annotation. - -=== The problem fact and planning entity properties - -Furthermore, OptaPlanner needs to know which `Lesson` instances it can change -as well as how to retrieve the `Timeslot` and `Room` instances used for score calculation -by your `TimeTableConstraintProvider`. - -The `timeslotList` and `roomList` fields have an `@ProblemFactCollectionProperty` annotation, -so your `TimeTableConstraintProvider` can select _from_ those instances. - -The `lessonList` has an `@PlanningEntityCollectionProperty` annotation, -so OptaPlanner can change them during solving -and your `TimeTableConstraintProvider` can select _from_ those too. - -== Create the solver service - -Now you are ready to put everything together and create a REST service. -But solving planning problems on REST threads causes HTTP timeout issues. -Therefore, the Quarkus extension injects a `SolverManager` instance, -which runs solvers in a separate thread pool -and can solve multiple datasets in parallel. - -Create the `src/main/java/org/acme/optaplanner/rest/TimeTableResource.java` class: - -[source,java] ----- -package org.acme.optaplanner.rest; - -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import jakarta.inject.Inject; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; - -import org.acme.optaplanner.domain.TimeTable; -import org.optaplanner.core.api.solver.SolverJob; -import org.optaplanner.core.api.solver.SolverManager; - -@Path("/timeTable") -public class TimeTableResource { - - @Inject - SolverManager solverManager; - - @POST - @Path("/solve") - public TimeTable solve(TimeTable problem) { - UUID problemId = UUID.randomUUID(); - // Submit the problem to start solving - SolverJob solverJob = solverManager.solve(problemId, problem); - TimeTable solution; - try { - // Wait until the solving ends - solution = solverJob.getFinalBestSolution(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException("Solving failed.", e); - } - return solution; - } - -} ----- - -For simplicity's sake, this initial implementation waits for the solver to finish, -which can still cause an HTTP timeout. -The _complete_ implementation avoids HTTP timeouts much more elegantly. - -== Set the termination time - -Without a termination setting or a termination event, the solver runs forever. -To avoid that, limit the solving time to five seconds. -That is short enough to avoid the HTTP timeout. - -Create the `src/main/resources/application.properties` file: - -[source,properties] ----- -# The solver runs only for 5 seconds to avoid an HTTP timeout in this simple implementation. -# It's recommended to run for at least 5 minutes ("5m") otherwise. -quarkus.optaplanner.solver.termination.spent-limit=5s ----- - - -== Run the application - -First start the application: - -include::{includes}/devtools/dev.adoc[] - -=== Try the application - -Now that the application is running, you can test the REST service. -You can use any REST client you wish. -The following example uses the Linux command `curl` to send a POST request: - -[source,shell] ----- -$ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}' ----- - -After about five seconds, according to the termination spent time defined in your `application.properties`, -the service returns an output similar to the following example: - -[source] ----- -HTTP/1.1 200 -Content-Type: application/json -... - -{"timeslotList":...,"roomList":...,"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room A"}},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room A"}},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room B"}},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room B"}}],"score":"0hard/0soft"} ----- - -Notice that your application assigned all four lessons to one of the two time slots and one of the two rooms. -Also notice that it conforms to all hard constraints. -For example, M. Curie's two lessons are in different time slots. - -On the server side, the `info` log show what OptaPlanner did in those five seconds: - -[source,options="nowrap"] ----- -... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). -... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). -... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). -... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). ----- - -=== Test the application - -A good application includes test coverage. - -==== Test the constraints - -To test each constraint in isolation, use a `ConstraintVerifier` in unit tests. -It tests each constraint's corner cases in isolation from the other tests, -which lowers maintenance when adding a new constraint with proper test coverage. - -Add a `optaplanner-test` dependency in your build file: - -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - org.optaplanner - optaplanner-test - test - ----- - -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -testImplementation("org.optaplanner:optaplanner-test") ----- - -Create the `src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java` class: - -[source,java] ----- -package org.acme.optaplanner.solver; - -import java.time.DayOfWeek; -import java.time.LocalTime; - -import jakarta.inject.Inject; - -import io.quarkus.test.junit.QuarkusTest; -import org.acme.optaplanner.domain.Lesson; -import org.acme.optaplanner.domain.Room; -import org.acme.optaplanner.domain.TimeTable; -import org.acme.optaplanner.domain.Timeslot; -import org.junit.jupiter.api.Test; -import org.optaplanner.test.api.score.stream.ConstraintVerifier; - -@QuarkusTest -class TimeTableConstraintProviderTest { - - private static final Room ROOM = new Room("Room1"); - private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON); - private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON); - - @Inject - ConstraintVerifier constraintVerifier; - - @Test - void roomConflict() { - Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1"); - Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2"); - Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3"); - - firstLesson.setRoom(ROOM); - firstLesson.setTimeslot(TIMESLOT1); - - conflictingLesson.setRoom(ROOM); - conflictingLesson.setTimeslot(TIMESLOT1); - - nonConflictingLesson.setRoom(ROOM); - nonConflictingLesson.setTimeslot(TIMESLOT2); - - constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict) - .given(firstLesson, conflictingLesson, nonConflictingLesson) - .penalizesBy(1); - } - -} ----- - -This test verifies that the constraint `TimeTableConstraintProvider::roomConflict`, -when given three lessons in the same room, where two lessons have the same timeslot, -it penalizes with a match weight of `1`. -So with a constraint weight of `10hard` it would reduce the score by `-10hard`. - -Notice how `ConstraintVerifier` ignores the constraint weight during testing - even -if those constraint weights are hard coded in the `ConstraintProvider` - because -constraints weights change regularly before going into production. -This way, constraint weight tweaking does not break the unit tests. - -==== Test the solver - -In a JUnit test, generate a test dataset and send it to the `TimeTableResource` to solve. - -Create the `src/test/java/org/acme/optaplanner/rest/TimeTableResourceTest.java` class: - -[source,java] ----- -package org.acme.optaplanner.rest; - -import java.time.DayOfWeek; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; - -import jakarta.inject.Inject; - -import io.quarkus.test.junit.QuarkusTest; -import org.acme.optaplanner.domain.Room; -import org.acme.optaplanner.domain.Timeslot; -import org.acme.optaplanner.domain.Lesson; -import org.acme.optaplanner.domain.TimeTable; -import org.acme.optaplanner.rest.TimeTableResource; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@QuarkusTest -public class TimeTableResourceTest { - - @Inject - TimeTableResource timeTableResource; - - @Test - @Timeout(600_000) - public void solve() { - TimeTable problem = generateProblem(); - TimeTable solution = timeTableResource.solve(problem); - assertFalse(solution.getLessonList().isEmpty()); - for (Lesson lesson : solution.getLessonList()) { - assertNotNull(lesson.getTimeslot()); - assertNotNull(lesson.getRoom()); - } - assertTrue(solution.getScore().isFeasible()); - } - - private TimeTable generateProblem() { - List timeslotList = new ArrayList<>(); - timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); - timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); - timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); - timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); - timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); - - List roomList = new ArrayList<>(); - roomList.add(new Room("Room A")); - roomList.add(new Room("Room B")); - roomList.add(new Room("Room C")); - - List lessonList = new ArrayList<>(); - lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); - lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); - lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); - lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); - lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); - - lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); - lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); - lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); - lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); - lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); - return new TimeTable(timeslotList, roomList, lessonList); - } - -} ----- - -This test verifies that after solving, all lessons are assigned to a time slot and a room. -It also verifies that it found a feasible solution (no hard constraints broken). - -Add test properties to the `src/main/resources/application.properties` file: - -[source,properties] ----- -quarkus.optaplanner.solver.termination.spent-limit=5s - -# Effectively disable spent-time termination in favor of the best-score-limit -%test.quarkus.optaplanner.solver.termination.spent-limit=1h -%test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft ----- - -Normally, the solver finds a feasible solution in less than 200 milliseconds. -Notice how the `application.properties` overwrites the solver termination during tests -to terminate as soon as a feasible solution (`0hard/*soft`) is found. -This avoids hard coding a solver time, because the unit test might run on arbitrary hardware. -This approach ensures that the test runs long enough to find a feasible solution, even on slow machines. -But it does not run a millisecond longer than it strictly must, even on fast machines. - -=== Logging - -When adding constraints in your `ConstraintProvider`, -keep an eye on the _score calculation speed_ in the `info` log, -after solving for the same amount of time, to assess the performance impact: - -[source] ----- -... Solving ended: ..., score calculation speed (29455/sec), ... ----- - -To understand how OptaPlanner is solving your problem internally, -change the logging in the `application.properties` file or with a `-D` system property: - -[source,properties] ----- -quarkus.log.category."org.optaplanner".level=debug ----- - -Use `debug` logging to show every _step_: - -[source,options="nowrap"] ----- -... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). -... CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]). -... CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]). -... ----- - -Use `trace` logging to show every _step_ and every _move_ per step. - -== Summary - -Congratulations! -You have just developed a Quarkus application with https://www.optaplanner.org/[OptaPlanner]! - -== Further improvements: Database and UI integration - -Now try adding database and UI integration: - -. Store `Timeslot`, `Room`, and `Lesson` in the database with xref:hibernate-orm-panache.adoc[Hibernate and Panache]. - -. xref:rest-json.adoc[Expose them through REST]. - -. Adjust the `TimeTableResource` to read and write a `TimeTable` instance in a single transaction -and use those accordingly: -+ -[source,java] ----- -package org.acme.optaplanner.rest; - -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; - -import io.quarkus.panache.common.Sort; -import org.acme.optaplanner.domain.Lesson; -import org.acme.optaplanner.domain.Room; -import org.acme.optaplanner.domain.TimeTable; -import org.acme.optaplanner.domain.Timeslot; -import org.optaplanner.core.api.score.ScoreManager; -import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; -import org.optaplanner.core.api.solver.SolverManager; -import org.optaplanner.core.api.solver.SolverStatus; - -@Path("/timeTable") -public class TimeTableResource { - - public static final Long SINGLETON_TIME_TABLE_ID = 1L; - - @Inject - SolverManager solverManager; - @Inject - ScoreManager scoreManager; - - // To try, open http://localhost:8080/timeTable - @GET - public TimeTable getTimeTable() { - // Get the solver status before loading the solution - // to avoid the race condition that the solver terminates between them - SolverStatus solverStatus = getSolverStatus(); - TimeTable solution = findById(SINGLETON_TIME_TABLE_ID); - scoreManager.updateScore(solution); // Sets the score - solution.setSolverStatus(solverStatus); - return solution; - } - - @POST - @Path("/solve") - public void solve() { - solverManager.solveAndListen(SINGLETON_TIME_TABLE_ID, - this::findById, - this::save); - } - - public SolverStatus getSolverStatus() { - return solverManager.getSolverStatus(SINGLETON_TIME_TABLE_ID); - } - - @POST - @Path("/stopSolving") - public void stopSolving() { - solverManager.terminateEarly(SINGLETON_TIME_TABLE_ID); - } - - @Transactional - protected TimeTable findById(Long id) { - if (!SINGLETON_TIME_TABLE_ID.equals(id)) { - throw new IllegalStateException("There is no timeTable with id (" + id + ")."); - } - // Occurs in a single transaction, so each initialized lesson references the same timeslot/room instance - // that is contained by the timeTable's timeslotList/roomList. - return new TimeTable( - Timeslot.listAll(Sort.by("dayOfWeek").and("startTime").and("endTime").and("id")), - Room.listAll(Sort.by("name").and("id")), - Lesson.listAll(Sort.by("subject").and("teacher").and("studentGroup").and("id"))); - } - - @Transactional - protected void save(TimeTable timeTable) { - for (Lesson lesson : timeTable.getLessonList()) { - // TODO this is awfully naive: optimistic locking causes issues if called by the SolverManager - Lesson attachedLesson = Lesson.findById(lesson.getId()); - attachedLesson.setTimeslot(lesson.getTimeslot()); - attachedLesson.setRoom(lesson.getRoom()); - } - } - -} ----- -+ -For simplicity's sake, this code handles only one `TimeTable` instance, -but it is straightforward to enable multi-tenancy and handle multiple `TimeTable` instances of different high schools in parallel. -+ -The `getTimeTable()` method returns the latest timetable from the database. -It uses the `ScoreManager` (which is automatically injected) -to calculate the score of that timetable, so the UI can show the score. -+ -The `solve()` method starts a job to solve the current timetable and store the time slot and room assignments in the database. -It uses the `SolverManager.solveAndListen()` method to listen to intermediate best solutions -and update the database accordingly. -This enables the UI to show progress while the backend is still solving. - -. Adjust the `TimeTableResourceTest` instance accordingly, now that the `solve()` method returns immediately. -Poll for the latest solution until the solver finishes solving: -+ -[source,java] ----- -package org.acme.optaplanner.rest; - -import jakarta.inject.Inject; - -import io.quarkus.test.junit.QuarkusTest; -import org.acme.optaplanner.domain.Lesson; -import org.acme.optaplanner.domain.TimeTable; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import org.optaplanner.core.api.solver.SolverStatus; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@QuarkusTest -public class TimeTableResourceTest { - - @Inject - TimeTableResource timeTableResource; - - @Test - @Timeout(600_000) - public void solveDemoDataUntilFeasible() throws InterruptedException { - timeTableResource.solve(); - TimeTable timeTable = timeTableResource.getTimeTable(); - while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) { - // Quick polling (not a Test Thread Sleep anti-pattern) - // Test is still fast on fast machines and doesn't randomly fail on slow machines. - Thread.sleep(20L); - timeTable = timeTableResource.getTimeTable(); - } - assertFalse(timeTable.getLessonList().isEmpty()); - for (Lesson lesson : timeTable.getLessonList()) { - assertNotNull(lesson.getTimeslot()); - assertNotNull(lesson.getRoom()); - } - assertTrue(timeTable.getScore().isFeasible()); - } - -} ----- - -. Build an attractive web UI on top of these REST methods to visualize the timetable. - -Take a look at link:{quickstarts-tree-url}/optaplanner-quickstart[the quickstart source code] to see how this all turns out.