Dev UI for extension developers

Dev UI v2

本指南涵盖 Dev UI v2,这是 Quarkus 3 及更高版本中的默认设置。

本指南涵盖 extension authors的 Quarkus Dev UI。 Quarkus 随附一个开发人员 UI,在 dev 模式下可用(在使用 `mvn quarkus:dev`启动 quarkus 时),默认情况下位于 /q/dev-ui。它将显示类似这样的内容:

dev ui overview v2

它允许你:

  • 快速可视化当前加载的所有扩展

  • 查看扩展状态,并直接转到扩展文档

  • view and change Configuration

  • 管理和可视化 Continuous Testing

  • view Dev Services information

  • view the Build information

  • 查看和流式传输各种日志

将列出应用程序中使用的每个扩展。在那里,你可以找到指南、其他信息以及每个扩展的适用配置:

dev ui extension card v2

Make my extension extend the Dev UI

要使扩展在 Dev UI 中列出,你无需执行任何操作!

所以你可以随时从这里开始 :)

扩展可以:

这些是引用其他(Dev UI 之外的)数据的链接。此数据可以是 HTML 页面、文本或其他数据。

一个小例子是 SmallRye OpenAPI 扩展,它包含指向以 JSON 和 YAML 格式生成的 OpenAPI 架构以及指向 Swagger UI 的链接:

dev ui extension openapi v2

在构建时便已知与这些外部引用的链接。因此,要在卡片上添加这样的链接,请在你的扩展中添加以下构建步骤:

@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 始终确保仅在开发者模式下运行此构建步骤
2 要在卡片上添加任何内容,您必须返回/生成 CardPageBuildItem.
3 要添加链接,可以使用 addPage`方法,因为所有的链接都会转到“页面”。`Page`有一些构建器用来帮助构建一个页面。对于 `external`链接,使用 `externalPageBuilder.
4 添加外部链接的 url(在本例中,我们使用 NonApplicationRootPathBuildItem`来创建此链接,因为此链接位于可配置的非应用路径下,默认值为 `/q)。如果链接在 /q`下可用,请始终使用 `NonApplicationRootPathBuildItem.
5 您可以(可选)提示您要导航到的内容的内容类型。如果没有提示,将会进行标头调用来确定 MediaType
6 您可以添加图标。所有免费的 font-awesome 图标均可用。
Note about icons

如果您在 Font awesome处找到您的图标,您可以按如下方式映射:例如 <i class="fa-solid fa-house"></i>`将映射到 `font-awesome-solid:house,因此 fa`变成 `font-awesome,并且对于图标名称,删除 fa-.

Embedding external content

默认情况下,即使是外部链接也会在开发者界面中呈现(嵌入)。对于 HTML,页面将被呈现,并且任何其他内容都将使用 code-mirror显示,用以标记媒体类型。例如,YAML 格式的 OpenAPI 架构文档:

dev ui extension openapi embed v2

如果您不想嵌入内容,可以在页面构建器上使用 .doNotEmbed(),然后这将会在一个新标签页中打开链接。

上面的示例假设您在构建时知道要使用的链接。在某些情况下,您可能只在运行时才知道。在那种情况下,可以使用返回要添加的链接的 Communicating to the backend方法,并在创建链接时使用它。不要在页面构建器上使用 .url`方法,而要使用 `.dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName").

Adding labels

您可以使用页面构建器上的其中一个构建器方法向卡片中的链接添加一个选项标签。这些标签可以是

  • 静态的(在构建时已知).staticLabel("staticLabelValue")

  • 动态的(在运行时加载).dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")

  • 流动的(在运行时持续流式传输更新的值).streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")

对于动态和流动的标签,请参见 Communicating to the backend小节。

dev ui extension card label v2

Add full pages

您还可以链接到“内部”页面(与上述“外部”页面相对应)。这意味着您可以构建页面并添加数据和操作以便在开发者界面中呈现。

Build time data

要使构建时数据可以在您的整页中使用,您可以使用一个键和一个值将任何数据添加到 `CardPageBuildItem`中:

CardPageBuildItem pageBuildItem = new CardPageBuildItem();
pageBuildItem.addBuildTimeData("someKey", getSomeValueObject());

您可以为页面上在构建时需要知道的所有数据添加多个这样的键值对。

在开发者界面中添加整页内容有几种选项。从最基本的(良好开端)开始到完全成熟的 Web 组件(首选)。

Display some build time data on a screen (without having to do frontend coding):

如果您有一些在构建时已知并希望显示的数据,则可以在 `Page`中使用以下某个构建器:

Raw data

这会显示您数据中原始的(已序列化的)JSON 值:

cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") (1)
                .icon("font-awesome-brands:js")
                .buildTimeDataKey("someKey")); (2)
1 Use the rawDataPageBuilder.
2 在页面构建项的 `addBuildTimeData`中添加构建时数据时,可返回链接至所使用的键。

这会创建一个链接到将原始数据呈现为 JSON 格式的页面:

dev ui raw page v2

Table data

如果结构允许,您还可以将构建时数据显示在表中:

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 可以选择仅显示某些字段。
3 在页面构建项的 `addBuildTimeData`中添加构建时数据时,可返回链接至所使用的键。

这会创建一个链接到将数据呈现为表格的页面:

dev ui table page v2

Qute data

您还可以使用 Qute 模板显示构建时数据。模板中可以使用所有构建时数据键:

cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") (1)
                .icon("font-awesome-solid:q")
                .templateLink("qute-jokes-template.html")); (2)
1 Use the quteDataPageBuilder.
2 在 `/deployment/src/main/resources/dev-ui/`中链接到 Qute 模板。

使用任何 Qute 模板来显示数据,例如 qute-jokes-template.html

<table>
    <thead>
        <tr>
            <th>Timestamp</th>
            <th>User</th>
            <th>Joke</th>
        </tr>
    </thead>
    <tbody>
        {#for joke in jokes} (1)
        <tr>
            <td>{joke.timestamp}</td>
            <td><span><img src="{joke.profilePic}" height="30px"></img> {joke.user}</span></td>
            <td>{joke.fullJoke}</td>
        </tr>
        {/for}
    </tbody>
</table>
1 `jokes`作为构建时数据键添加到页面构建项中。

Web component page

如需使用操作和运行时(或构建时)数据构建交互页面,您必须使用 Web 组件页面:

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 /deployment/src/main/resources/dev-ui/`中链接到 Web 组件。还可定义标题(在构建器中使用 `.title("My title")),但如果不定义,标题将根据 componentLink 推测,而 componentLink 的格式应始终为 qwc(表示 Quarkus Web 组件)破折号 extensionName (例如,在本例中为 arc)破折号 page title(在本例中为“Beans”)

Dev UI 使用 Lit简化了构建这些 Web 组件的过程。您可以在此处详细了解 Web 组件和 Lit:

Basic structure of a Web component page

Web 组件页面只是一个会创建一个新 HTML 元素的 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`<ul>
                ${this._beans.map((bean) => (9)
                    html`<li>${bean.providerType.name}</li>`
                )}</ul>`;
        } else {
            return html`No beans found`;
        }
    }
}
customElements.define('qwc-arc-beans', QwcArcBeans); (10)
1 您可以从其他库导入类和/或函数。在本例中,我们从 `Lit`中使用了 `LitElement`类和 `html`和 `css`函数
2 构建时数据在构建步骤中定义,并且可以使用键从 `build-time-data`处导入。构建步骤中添加的所有键均可用。
3 组件应采用以下格式命名:Qwc(表示 Quarkus Web 组件),然后是扩展名,再是页面标题,所有这些使用骆驼拼写法串联在一起。这还将与之前描述的文件名格式相匹配。组件还应扩展 LitComponent
4 可以使用 `css`函数添加 CSS 样式,并且这些样式仅应用于您的组件。
5 样式可以引用全局定义的 CSS 变量,以确保正确地渲染您的网页,尤其是在浅色和深色模式之间切换时。您可以在 Vaadin 文档(\ Color、\ Sizing and Spacing等)中找到所有 CSS 变量。
6 可以添加属性。如果属性为私有,则可在其前面使用`_`。属性通常注入到 HTML 模板中,并且可以定义为具有状态,也就是说,如果该属性发生变化,则该组件应该重新渲染。在这种情况下,bean 为构建时数据,并且只在热重新加载时更改,这将在后面介绍。
7 构造函数(可选)始终应该首先调用`super`,然后设置属性的默认值。
8 将调用渲染方法(来自`LitElement`)来渲染该页面。在此方法中,您可以返回所需页面的标记。您可以使用来自\`Lit`的\`html`函数,该函数为您提供模板语言以输出所需的 HTML。创建模板后,您只需设置或更改属性即可重新渲染页面内容。了解有关\ Lit html的更多信息
9 您可以使用内置模板函数来进行条件、列表等。了解有关\ Lit Templates的更多信息
10 您必须始终以唯一的标签将 Web 组件注册为自定义元素。在此处,标签将遵循与文件名相同格式(`qwc`破折号\`extension name`破折号\`page title`);
Using Vaadin UI components for rendering

Dev UI 广泛使用\ Vaadin web components作为 UI 构建模块。

例如,弧 Bean 使用\ Vaadin Grid渲染:

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 '@qomponent/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`
                <vaadin-grid .items="${this._beans}" class="arctable" theme="no-border">
                    <vaadin-grid-column auto-width
                        header="Bean"
                        ${columnBodyRenderer(this._beanRenderer, [])}
                        resizable>
                    </vaadin-grid-column>

                    <vaadin-grid-column auto-width
                        header="Kind"
                        ${columnBodyRenderer(this._kindRenderer, [])}
                        resizable>
                    </vaadin-grid-column>

                    <vaadin-grid-column auto-width
                        header="Associated Interceptors"
                        ${columnBodyRenderer(this._interceptorsRenderer, [])}
                        resizable>
                    </vaadin-grid-column>
                </vaadin-grid>`;

        } else {
            return html`No beans found`;
        }
    }

    _beanRenderer(bean) {
        return html`<vaadin-vertical-layout>
      <code class="annotation">@${bean.scope.simpleName}</code>
      ${bean.nonDefaultQualifiers.map(qualifier =>
            html`${this._qualifierRenderer(qualifier)}`
        )}
      <code>${bean.providerType.name}</code>
      </vaadin-vertical-layout>`;
    }

    _kindRenderer(bean) {
      return html`
        <vaadin-vertical-layout>
          ${this._kindBadgeRenderer(bean)}
          ${this._kindClassRenderer(bean)}
        </vaadin-vertical-layout>
    `;
    }

    _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`<qui-badge level='${level}' small><span>${kind}</span></qui-badge>`
          : html`<qui-badge small><span>${kind}</span></qui-badge>`
        }`;
    }

    _kindClassRenderer(bean){
      return html`
          ${bean.declaringClass
            ? html`<code class="producer">${bean.declaringClass.simpleName}.${bean.memberName}()</code>`
            : html`<code class="producer">${bean.memberName}</code>`
          }
      `;
    }

    _interceptorsRenderer(bean) {
        if (bean.interceptors && bean.interceptors.length > 0) {
            return html`<vaadin-vertical-layout>
                          ${bean.interceptorInfos.map(interceptor =>
                              html`<div>
                                    <code>${interceptor.interceptorClass.name}</code>
                                    <qui-badge class="${bean.kind.toLowerCase()}" small pill><span>${interceptor.priority}</span></qui-badge>
                                  </div>`
                          )}
                        </vaadin-vertical-layout>`;
        }
    }

    _qualifierRenderer(qualifier) {
        return html`<code class="annotation">${qualifier.simpleName}</code>`;
    }

    _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 导入您想要使用的 Vaadin 组件
2 您还可以导入其他函数(如果需要的话)
3 您还可以使用 Qomponent 库中的任何组件,如下所述
Qomponent

我们还包括 [Qomponent]([role="bare"] \[role="bare"]https://github.com/qomponent)库中的所有组件

Card

卡片组件以在卡片中显示内容

import '@qomponent/qui-card';
    <qui-card header="Small">
        <div slot="content">
            <div class="cardcontents">
                <span>Hello</span>
            </div>
        </div>
    </qui-card>
Badge

徽章 UI 组件基于\ vaadin themed徽章

dev ui qui badge v2
import '@qomponent/qui-badge';

您可以使用小、主要、药丸的任意组合,带有图标和带任意级别的可点击性(`default`、`success`、`warning`、`error`、`contrast`),或设置您自己的颜色。

<div class="cards">
    <h3>Tiny</h3>
    <div class="card">
        <qui-badge tiny><span>Default</span></qui-badge>
        <qui-badge level="success" tiny><span>Success</span></qui-badge>
        <qui-badge level="warning" tiny><span>Warning</span></qui-badge>
        <qui-badge level="error" tiny><span>Error</span></qui-badge>
        <qui-badge level="contrast" tiny><span>Contrast</span></qui-badge>
        <qui-badge background="pink" color="purple" tiny><span>Custom colors</span></qui-badge>
    </div>

    <h3>Small</h3>
    <div class="card">
        <qui-badge small><span>Default</span></qui-badge>
        <qui-badge level="success" small><span>Success</span></qui-badge>
        <qui-badge level="warning" small><span>Warning</span></qui-badge>
        <qui-badge level="error" small><span>Error</span></qui-badge>
        <qui-badge level="contrast" small><span>Contrast</span></qui-badge>
        <qui-badge background="pink" color="purple" small><span>Custom colors</span></qui-badge>
    </div>

    <h3>Primary</h3>
    <div class="card">
        <qui-badge primary><span>Default primary</span></qui-badge>
        <qui-badge level="success" primary><span>Success primary</span></qui-badge>
        <qui-badge level="warning" primary><span>Warning primary</span></qui-badge>
        <qui-badge level="error" primary><span>Error primary</span></qui-badge>
        <qui-badge level="contrast" primary><span>Contrast primary</span></qui-badge>
        <qui-badge background="pink" color="purple" primary><span>Custom colors</span></qui-badge>
    </div>

    <h3>Pill</h3>
    <div class="card">
        <qui-badge pill><span>Default pill</span></qui-badge>
        <qui-badge level="success" pill><span>Success pill</span></qui-badge>
        <qui-badge level="warning" pill><span>Warning pill</span></qui-badge>
        <qui-badge level="error" pill><span>Error pill</span></qui-badge>
        <qui-badge level="contrast" pill><span>Contrast pill</span></qui-badge>
        <qui-badge background="pink" color="purple" pill><span>Custom colors</span></qui-badge>
    </div>

    <h3>With Icon</h3>
    <div class="card">
        <qui-badge text="Default" icon="circle-info">
            <span>Default icon</span>
        </qui-badge>
        <qui-badge text="Success" level="success" icon="circle-check">
            <span>Success icon</span>
        </qui-badge>
        <qui-badge text="Warning" level="warning" icon="warning">
            <span>Warning icon</span>
        </qui-badge>
        <qui-badge text="Error" level="error" icon="circle-exclamation">
            <span>Error icon</span>
        </qui-badge>
        <qui-badge text="Contrast" level="contrast" icon="adjust">
            <span>Contrast icon</span>
        </qui-badge>
        <qui-badge text="Custom" background="pink" color="purple" icon="flag-checkered">
            <span>Custom colors</span>
        </qui-badge>
    </div>

    <h3>Icon only</h3>
    <div class="card">
        <qui-badge icon="circle-info"></qui-badge>
        <qui-badge level="success" icon="circle-check"></qui-badge>
        <qui-badge level="warning" icon="warning"></qui-badge>
        <qui-badge level="error" icon="circle-exclamation"></qui-badge>
        <qui-badge level="contrast" icon="adjust"></qui-badge>
        <qui-badge level="contrast" background="pink" color="purple" icon="flag-checkered"></qui-badge>
    </div>

    <h3>Clickable</h3>
    <div class="card">
        <qui-badge clickable><span>Default</span></qui-badge>
        <qui-badge clickable level="success"><span>Success</span></qui-badge>
        <qui-badge clickable level="warning"><span>Warning</span></qui-badge>
        <qui-badge clickable level="error"><span>Error</span></qui-badge>
        <qui-badge clickable level="contrast"><span>Contrast</span></qui-badge>
        <qui-badge clickable background="pink" color="purple"><span>Custom colors</span></qui-badge>
    </div>

</div>
Alert

警报围绕 Bootstrap 警报进行建模。点击\ here了解更多信息。

此外,请查看下面的通知控制器作为一种替代方案。

dev ui qui alert v2
import '@qomponent/qui-alert';
<qui-alert><span>Info alert</span></qui-alert>
<qui-alert level="success"><span>Success alert</span></qui-alert>
<qui-alert level="warning"><span>Warning alert</span></qui-alert>
<qui-alert level="error"><span>Error alert</span></qui-alert>

<qui-alert permanent><span>Permanent Info alert</span></qui-alert>
<qui-alert level="success" permanent><span>Permanent Success alert</span></qui-alert>
<qui-alert level="warning" permanent><span>Permanent Warning alert</span></qui-alert>
<qui-alert level="error" permanent><span>Permanent Error alert</span></qui-alert>

<qui-alert center><span>Center Info alert</span></qui-alert>
<qui-alert level="success" center><span>Center Success alert</span></qui-alert>
<qui-alert level="warning" center><span>Center Warning alert</span></qui-alert>
<qui-alert level="error" center><span>Center Error alert</span></qui-alert>

<qui-alert showIcon><span>Info alert with icon</span></qui-alert>
<qui-alert level="success" showIcon><span>Success alert with icon</span></qui-alert>
<qui-alert level="warning" showIcon><span>Warning alert with icon</span></qui-alert>
<qui-alert level="error" showIcon><span>Error alert with icon</span></qui-alert>

<qui-alert icon="vaadin:flag-checkered"><span>Info alert with custom icon</span></qui-alert>
<qui-alert level="success" icon="vaadin:flag-checkered"><span>Success alert with custom icon</span></qui-alert>
<qui-alert level="warning" icon="vaadin:flag-checkered"><span>Warning alert with custom icon</span></qui-alert>
<qui-alert level="error" icon="vaadin:flag-checkered"><span>Error alert with custom icon</span></qui-alert>

<qui-alert size="small" showIcon><span>Small Info alert with icon</span></qui-alert>
<qui-alert level="success" size="small" showIcon><span>Small Success alert with icon</span></qui-alert>
<qui-alert level="warning" size="small" showIcon><span>Small Warning alert with icon</span></qui-alert>
<qui-alert level="error" size="small" showIcon><span>Small Error alert with icon</span></qui-alert>

<qui-alert showIcon><span>Info <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="success" showIcon><span>Success <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="warning" showIcon><span>Warning <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="error" showIcon><span>Error <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>

<qui-alert showIcon primary><span>Primary Info alert with icon</span></qui-alert>
<qui-alert level="success" showIcon primary><span>Primary Success alert with icon</span></qui-alert>
<qui-alert level="warning" showIcon primary><span>Primary Warning alert with icon</span></qui-alert>
<qui-alert level="error" showIcon primary><span>Primary Error alert with icon</span></qui-alert>

<qui-alert title="Information"><span>Info alert with title</span></qui-alert>
<qui-alert title="Well done" level="success"><span>Success alert with title</span></qui-alert>
<qui-alert title="Beware" level="warning"><span>Warning alert with title</span></qui-alert>
<qui-alert title="Ka-boom" level="error"><span>Error alert with title</span></qui-alert>

<qui-alert title="Information" showIcon><span>Info alert with title and icon</span></qui-alert>
<qui-alert title="Well done" level="success" showIcon><span>Success alert with title and icon</span></qui-alert>
<qui-alert title="Beware" level="warning" showIcon><span>Warning alert with title and icon</span></qui-alert>
<qui-alert title="Ka-boom" level="error" showIcon><span>Error alert with title and icon</span></qui-alert>
Code block

显示代码块。此组件了解主题并将使用正确的 codemirror 主题来匹配浅色/深色模式。

dev ui qui code block v2
import '@qomponent/qui-code-block';
<qui-code-block mode="properties">
    <slot>
        foo = bar
    </slot>
</qui-code-block>

或从 URL 获取内容:

<div class="codeBlock">
    <qui-code-block
        mode='${this._mode}'
        src='${this._externalUrl}'>
    </qui-code-block>
</div>

为了确保代码块采用正确的 code-mirror 主题(基于 Dev UI 中当前的主题),你可以执行以下操作:

import { observeState } from 'lit-element-state';
import { themeState } from 'theme-state';

然后将 extends 更改为观察状态:

extends observeState(LitElement) {

现在你可以获取当前主题,因此将 theme 属性添加到代码块,例如:

<div class="codeBlock">
    <qui-code-block
        mode='${this._mode}'
        src='${this._externalUrl}'
        theme='${themeState.theme.name}'>
    </qui-code-block>
</div>

模式:- 属性 - js - java - xml - json - yaml - sql - html - css - sass - markdown

Internal components

创建一个指向可以在用户 IDE 中打开的资源(如 Java 源文件)的链接(如果我们可以检测到 IDE)。

import 'qui-ide-link';
<qui-ide-link title='Source full class name'
                        class='text-source'
                        fileName='${sourceClassNameFull}'
                        lineNumber=${sourceLineNumber}>[${sourceClassNameFull}]</qui-ide-link>;
Using internal controllers

一些 internal controllers 可用于让某些事情变得更容易:

Notifier

这是显示土司消息的简单方法。土司可以放置在屏幕上(默认左下角),并且可以具有一定等级(信息、成功、警告、错误)。任何等级都可以是主要的,从而创建一个更突出的土司消息。

查看此控制器的源: here

示例用法:

dev ui controller notifier
import { notifier } from 'notifier';
<a @click=${() => this._info()}>Info</a>;
_info(position = null){
    notifier.showInfoMessage("This is an information message", position);
}

你可以在 here 找到所有有效位置。

Storage

以安全方式访问本地存储的简单方法。这将值存储在本地存储中,该存储适用于你的扩展程序。这样,你就不必担心与另一个扩展程序发生冲突了。

本地存储对于记住用户偏好或状态很有用。例如,页脚会记住状态(打开/关闭)和底部抽屉打开时的尺寸。

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
Log

日志控制器用于向 (页脚) 日志添加控制按钮。请参阅 Add a log file

dev ui log control v2
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();
Router

路由器主要在内部使用。这使用 Vaadin Router 在幕后将 URL 路由到 SPA 中正确的页面/部分。它将更新导航并允许历史记录(返回按钮)。这也创建了在具有多个页面的扩展程序上可用的子菜单。

请参阅 controller 以获取一些可能有用的方法。

Communicating to the backend

JsonRPC against the Runtime classpath

您可以获取或流式处理运行时数据(而非前面讨论的 Build time data),或针对运行时类路径(而非部署类路径)执行方法。在运行时获取数据分为两部分。运行时模块中的 Java 端,然后是 Web 组件中的用法。

Java part

此代码负责提供数据以在 UI 上显示。

您必须在部署模块中 Processor 中注册 JsonPRCService

@BuildStep(onlyIf = IsDevelopment.class)(1)
JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {(2)
    return new JsonRPCProvidersBuildItem(CacheJsonRPCService.class);(3)
}
1 始终仅在开发模式中执行此操作
2 生成或返回 JsonRPCProvidersBuildItem
3 在运行时模块中定义该类,它将包含在 UI 中提供数据的方法

现在,在您的运行时模块中,创建 JsonRPC 服务。此类将默认为一个应用程序范围的 Bean,除非您明确指定 Bean 的范围。返回某些内容的所有 public 方法均可供 Web 组件 Javascript 调用。

这些方法中的返回对象可以是:

  • primitives or String,

  • io.vertx.core.json.JsonArray

  • io.vertx.core.json.JsonObject

  • 任何可序列化为 JSON 的其他 POJO

以上所有内容都可以是阻塞(POJO)或非阻塞(@NonBlocking`或 `Uni)。另外,可以使用 `Multi`流式传输数据。

@NonBlocking (1)
public JsonArray getAll() { (2)
    Collection<String> names = manager.getCacheNames();
    List<CaffeineCache> allCaches = new ArrayList<>(names.size());
    for (String name : names) {
        Optional<Cache> 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 此示例以非阻塞模式运行。我们也可以返回 Uni&lt;JsonArray&gt;
2 方法名 `getAll`将可用于 Javascript

Webcomponent (Javascript) part

现在您可以使用 JsonRPC 控制器访问 `getAll`方法(以及 JsonRPC 服务中的任何其他方法)

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 请注意,方法 `getAll`对应于 Java 服务中的方法。该方法返回一个带有 JsonRPC 结果的 Promise
2 在这种情况下,结果是一个数组,因此我们可以遍历它。

JsonArray(或任意 Java 集合)都将返回一个数组;否则,将返回一个 JsonObject。

你也可以在被调用的方法中传递参数,例如:(在 Runtime Java 代码中)

public Uni<JsonObject> clear(String name) { (1)
    Optional<Cache> 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 clear 方法接受一个名为 name 的参数

在 Webcomponent(Javascript)中:

_clear(name) {
    this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { (1)
        this._updateCache(jsonRpcResponse.result)
    });
}
1 传递 name 参数。

Streaming data

你可以通过持续向屏幕流送数据,保持 UI 屏幕更新为最新数据。这可以通过 Multi (Java 端) 和 Observer (Javascript 端) 来实现

流式传输数据的 Java 端:

public class JokesJsonRPCService {

    private final BroadcastProcessor<Joke> jokeStream = BroadcastProcessor.create();

    @PostConstruct
    void init() {
        Multi.createFrom().ticks().every(Duration.ofHours(4)).subscribe().with((item) -> {
            jokeStream.onNext(getJoke());
        });
    }

    public Multi<Joke> streamJokes() { (1)
        return jokeStream;
    }

    // ...
}
1 返回将流送笑话的 Multi

流式传输数据的 Javascript 端:

this._observer = this.jsonRpc.streamJokes().onNext(jsonRpcResponse => { (1)
    this._addToJokes(jsonRpcResponse.result);
    this._numberOfJokes = this._numberOfJokes++;
});

// ...

this._observer.cancel(); (2)
1 你可以调用方法(可选地传递参数),然后提供将在下一个事件中被调用的代码。
2 确保保留观察者的一个实例,以便在需要时稍后取消。

JsonRpc against the deployment classpath

在某些情况下,你可能需要针对部署类路径执行方法和/或获取数据。这也会通过 JsonRPC 通信发生,但在这种情况下,你无需在 runtime 模块中创建 JsonRPC 服务,你可以直接在部署模块中的供应商中提供要运行的代码。要做到这一点,你需要生成一个 BuildTimeActionBuildItem,例如:

    @BuildStep(onlyIf = IsDevelopment.class)
    BuildTimeActionBuildItem createBuildTimeActions() { (1)
        BuildTimeActionBuildItem generateManifestActions = new BuildTimeActionBuildItem();(2)
        generateManifestActions.addAction("generateManifests", ignored -> { (3)
            try {
                List<Manifest> manifests = holder.getManifests();
                // Avoid relying on databind.
                Map<String, String> map = new LinkedHashMap<>();
                for (Manifest manifest : manifests) {
                    map.put(manifest.getName(), manifest.getContent());
                }
                return map;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        return generateManifestActions;
    }
1 返回或使用一个 BuildProducer 来创建一个 BuildTimeActionBuildItem
2 BuildTimeActionBuildItem 会自动使用你的扩展命名空间进行限定
3 这里我们添加一个动作,它与请求响应方法相同。方法名(可以在 js 中以与任何 json-rpc 服务相同的方式调用)是 generateManifests

你还可以将 CompletableFuture/CompletionStage 返回为一个动作,如果你想流送数据,你需要使用 addSubscription (而不是 addAction)并返回一个 Flow.Publisher

Dev UI Log

当使用 999-SNAPSHOT 版本运行本地应用程序时,开发人员界面将在页脚中显示一个 Dev UI 日志。这对于调试浏览器和 Quarkus 应用程序之间流动的所有 JSON RPC 消息很有用。

dev ui jsonrpc log v2

Hot reload

当热重载发生时,可以自动更新屏幕。为此,用 QwcHotReloadElement 替换 Webcomponent 扩展的 LitElement

QwcHotReloadElement 扩展 LitElement,所以组件仍然是 Lit 元素。

当扩展 QwcHotReloadElement 时,必须使用 hotReload 方法。(还必须仍然提供 Lit 的 render 方法)

import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';

// ...

export class QwcMyExtensionPage extends QwcHotReloadElement {

    render(){
        // ...
    }

    hotReload(){
        // ..
    }

}

Custom cards

如果不想使用默认内置卡片,可以自定义在扩展页面上显示的卡片。

为此,必须提供一个将加载到所提供卡片位置中的 Webcomponent,并在 Java 处理器中注册此卡片:

cardPageBuildItem.setCustomCard("qwc-mycustom-card.js");

在 JavaScript 端,可以访问所有页面(在想要创建链接时)

import { pages } from 'build-time-data';

并将传入以下属性:

  • extensionName

  • description

  • guide

  • namespace

static properties = {
    extensionName: {type: String},
    description: {type: String},
    guide: {type: String},
    namespace: {type: String}
}

State (advance)

State 允许属性包含状态并且可以在全局范围内重复使用。状态属性的一个示例是主题、连接状态(如果已连接到后端)等。

请参见 current built-in 状态对象。

Dev UI 中的状态使用 LitState。可以在其 documentation 中详细了解它。

Add a log file

除了添加卡片和页面之外,扩展还可以将日志添加到页脚中。这对于记录持续发生的事件很有用。离开某个页面时,页面将丢失与后端的连接,而页脚中的日志将永久连接。

将某些内容添加到页脚的工作方式与添加卡片完全相同,除了使用 FooterPageBuildItem 而不是 CardPageBuildItem

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);

然后可以在 Webcomponent 中将日志流传输到界面:

export class QwcJokesLog extends LitElement {
    jsonRpc = new JsonRpc(this);
    logControl = new LogController(this);

    // ....
}

Add a section menu

这允许扩展在菜单部分中直接链接到页面。

将某些内容添加到部分菜单的工作方式与添加卡片完全相同,除了使用 MenuPageBuildItem 而不是 CardPageBuildItem

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);

您的页面可以是任何类似于卡片的页面。

Testing

您可以在扩展中添加用来测试的测试:

  • Build time data

  • Runtime data via JsonRPC

您必须将其添加到您的 pom:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx-http-dev-ui-tests</artifactId>
    <scope>test</scope>
</dependency>

这会让您访问两个基本类以创建这些测试。

Testing Build time data

如果您添加了构建时间数据,例如:

cardPageBuildItem.addBuildTimeData("somekey", somevalue);

如果您要测试您的构建时间数据是否正确生成,您可以添加一个扩展 `DevUIBuildTimeDataTest`的测试。

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

如果您添加了一个带运行时数据响应的 JsonRPC 服务,例如:

public boolean updateProperties(String content, String type) {
    // ...
}

如果您要测试 `updateProperties`是否通过 JsonRPC 正确执行,您可以添加一个扩展 `DevUIJsonRPCTest`的测试。

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);
    }
}