The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

エクステンション開発者のためのDev UI

Dev UI v2

このガイドでは、Quarkus 3以降のデフォルトであるDev UI v2について説明します。

このガイドでは、 エクステンションの作成者向けにQuarkus Dev UIについて説明します。

Quarkus には Developer UI が搭載されており、dev モード (mvn quarkus:dev で Quarkus を起動) で使用可能で、 デフォルトで /q/dev-ui に存在し、次のような画面が表示されます。

Dev UI overview

これにより、次のことが可能になります:

  • 現在ロードされているすべてのエクステンションを素早く視覚化

  • エクステンションのステータス表示とドキュメントへの直接移動

  • Configuration の表示と変更

  • 管理と可視化 Continuous Testing

  • Dev Services の情報の表示

  • ビルド情報の表示

  • 各種ログの表示とストリーミング

アプリケーションで使用されるエクステンションがリストされます。各エクステンションのガイド、追加情報、適用可能な設定が表示されます。

Dev UI extension card

エクステンションで Dev UI を拡張する

何をしなくても、Dev UI にエクステンションが表示されます。

つまり、いつでもそのまま使用できます :)

エクステンションは以下を実行できます。

これらは、他の (Dev UI の外の) データを参照するリンクで、HTML ページ、テキスト、その他のデータがあります。

その良い例としては、JSON と YAML の両方のフォーマットで生成された OpenAPI スキーマへのリンクと、Swagger UI へのリンクが含まれる SmallRye OpenAPI エクステンションが挙げられます。

Dev UI extension card

外部参照へのリンクはビルド時に判明するため、このようなリンクをカードに表示するには、エクステンションに以下のビルドステップを追加します。

@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 このビルドステップは、dev モードでのみ実行されることを必ず確認してください。
2 カードに何かを追加するには、 CardPageBuildItem を返す/生成する必要があります。
3 リンクを追加するには addPage メソッドを使用します。すべてのリンクが "page" に移動します。 Page には、ページの構築を支援するビルダーがいくつかあります。 external リンクの場合は、 externalPageBuilder を使用します。
4 外部リンクの URL を追加します。ここでは、リンクが設定可能な非アプリケーションパス (デフォルトは /q) の下にあるため、 NonApplicationRootPathBuildItem を使用してリンクを作成します。リンクが /q の下にある場合は、必ず NonApplicationRootPathBuildItem を使用します。
5 移動先のコンテンツのコンテンツタイプを (オプションで) ヒントとして表示できます。ヒントがない場合、 MediaType を決定するためにヘッダー呼び出しが実行されます。
6 アイコンを追加できます。無料の font-awesome アイコンが利用可能です。
アイコンに関する注意事項
Font awesome でアイコンを見つけた場合は、次のようにマッピングできます。たとえば、 <i class="fa-solid fa-house"></i>font-awesome-solid:house にマッピングされるため、 fafont-awesome になり、アイコン名から fa- が削除されます。

外部コンテンツの埋め込み

デフォルトでは、外部リンクも Dev UI の内部 (埋め込み) でレンダリングされます。HTML の場合、ページがレンダリングされ、その他のコンテンツはメディアタイプをマークアップするために code-mirror を使用して表示されます。たとえば yaml 形式のOpenAPI スキーマドキュメントは、次のようになります。

Dev UI embedded page

コンテンツを埋め込みたくない場合は、ページビルダーの .doNotEmbed()、リンクを新しいタブで開くことができます。

上記の例では、ビルド時に使用するリンクが判明していることを前提としていますが、そうでないこともあります。その場合は、追加するリンクを返す バックエンドとの通信 を使用できます。これは、リンクの作成時に使用します。ページビルダーで .url メソッドを使用する代わりに、 .dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName") を使用します。

ラベルの追加

ページビルダーのビルダーメソッドの 1 つを使用して、カード内のリンクにオプションラベルを追加できます。以下のラベルがあります。

  • 静的 (ビルド時に既知のラベル) .staticLabel("staticLabelValue")

  • 動的 (実行時にロード) .dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")

  • ストリーミング (実行時に更新される値の継続的なストリーミング)値を継続的にストリーミング .streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")

動的ラベルとストリーミングラベルについては、バックエンドとの通信 セクションを参照してください。

Dev UI card labels

ページ全体の追加

"内部" ページ (上記の "外部" ページとは対照的) にリンクすることもできます。つまり、ページをビルドし、Dev UI でレンダリングするためのデータとアクションを追加できます。

ビルド時データ

ビルド時データをページ全体で利用できるようにするには、キーと値を使用して任意のデータを CardPageBuildItem に追加します。

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

ページに必要なことがビルド時にわかっているすべてのデータに対して、これらのキーと値のペアを複数追加できます。

Dev UI にページ全体のコンテンツを追加する方法としては、いくつかのオプションがあります。最も基本的なもの (入門向け) から本格的な Web コンポーネント (推奨) まであります。

ビルド時データを画面に表示します (フロントエンドのコーディングは不要)。

ビルド時に表示したいデータがわかっている場合は、 Page で次のいずれかのビルダーを使用できます。

生データ

データは、生の (シリアル化された) JSON 値で表示されます。

cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") (1)
                .icon("font-awesome-brands:js")
                .buildTimeDataKey("someKey")); (2)
1 rawDataPageBuilder を使用します。
2 Page BuildItem の addBuildTimeData でビルド時データを追加したときに使用したキーにリンクします。

これにより、生データを JSON でレンダリングするページへのリンクが作成されます。

Dev UI raw page

テーブルデータ

構造上可能な場合は、ビルド時データをテーブルに表示することもできます。

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 tableDataPageBuilder を使用します。
2 オプションで特定のフィールドのみを表示します。
3 Page BuildItem の addBuildTimeData でビルド時データを追加したときに使用したキーにリンクします。

これにより、テーブル内のデータをレンダリングするページへのリンクが作成されます。

Dev UI table page

Qute データ

qute テンプレートを使用して、ビルド時データを表示することもできます。すべてのビルド時データキーは、テンプレートで使用できます。

cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") (1)
                .icon("font-awesome-solid:q")
                .templateLink("qute-jokes-template.html")); (2)
1 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 Page Build Item のビルド時データキーとして jokes が追加されました。

Web コンポーネントページ

アクションとランタイム (またはビルド時) データを含むインタラクティブページをビルドするには、Web コンポーネントページを使用する必要があります。

cardPageBuildItem.addPage(Page.webComponentPageBuilder() (1)
                    .icon("font-awesome-solid:egg")
                    .componentLink("qwc-arc-beans.js") (2)
                    .staticLabel(String.valueOf(beans.size())));
1 webComponentPageBuilder を使用します。
2 /deployment/src/main/resources/dev-ui/ の Web コンポーネントへのリンク。タイトルも (ビルダーで .title("My title") を使用して定義できますが、定義しない場合は componentLink からタイトルが取得されます。componentLink の形式は必ず qwc (Quarkus Web Component の略) ダッシュ extensionName (この例では arc) ダッシュ page title (ここでは "Beans") です。

Dev UI は、これらの Web コンポーネントの構築を容易にするために Lit を使用します。Web コンポーネントと Lit の詳細は、以下を参照してください。

Web コンポーネントページの基本構造

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 他のライブラリーからクラスや関数をインポートできます。 その場合、 LitLitElement クラスと html および css 関数を使用します。
2 ビルド・ステップで定義されたビルド・タイム・データは、キーを使用して build-time-data から常にインポートすることができます。ビルド・ステップで追加されたすべてのキーが使用可能になります。
3 コンポーネントの名前は、Qwc (Quarkus Web Component の略)、エクステンション名、ページタイトルの順に、すべて Camel Case で連結した形式にする必要があります。これは、前述のファイル名の形式とも一致します。コンポーネントは LitComponent を拡張する必要もあります。
4 CSS スタイルは css 関数を使用して追加でき、これらのスタイルはコンポーネントにのみ適用されます。
5 スタイルは、グローバルに定義された CSS 変数を参照して、特にライトモードとダークモードの切り替え時にページが正しくレンダリングされるようにすることができます。すべての CSS 変数は、Vaadin のドキュメント ((カラー・サイズと間隔など) で確認できます。
6 プロパティを追加することができます。プロパティがプライベートである場合は、プロパティの前に _ を使用します。プロパティは通常HTMLテンプレートに注入され、そのプロパティが変更された場合、コンポーネントを再レンダリングすることを意味する状態を持つように定義できます。この場合、ビーンズはビルド時間データであり、ホットリロード時にのみ変更されます。
7 コンストラクター (オプション) は必ず最初に super を呼び出し、次にそのプロパティーのデフォルト値を設定する必要があります。
8 ページをレンダリングするために、render メソッドが (LitElement から) 呼び出されます。このメソッドでは、必要なページのマークアップを返します。 Lithtml 関数を使用すると、必要な HTML を出力するためのテンプレート言語を取得できます。テンプレートの作成後は、プロパティーを設定または変更するだけでページコンテンツを再レンダリングできます。詳細は、 Lit html を参照してください。
9 組み込みのテンプレート関数を使用して、conditional、list などを実行できます。詳細は、 Lit テンプレート を参照してください。
10 Web コンポーネントは、必ず一意のタグを持つカスタム要素として登録する必要があります。その場合のタグは、ファイル名と同じ形式 (qwc ダッシュ エクステンション名 ダッシュ ページタイトル) である必要があります。
レンダリングに Vaadin UI コンポーネントを使用する

Dev UI では、UI ビルディングブロックとして Vaadin Web コンポーネント を広範に使用します。

たとえば、Arc Beans は 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 ライブラリーのすべてのコンポーネントも含まれています。

Card

カード内のコンテンツを表示する Card コンポーネント。

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

vaadin themed バッジに基づく Badge UI コンポーネント。

Dev UI Badge
import '@qomponent/qui-badge';

small、primary、pill、アイコン付き、クリッカブル を defaultsuccesswarningerrorcontrast の任意のレベルと組み合わせて使用することも、独自のカラーを設定することもできます。

<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アラートをモデルにしています。詳しくは こちら をクリックしてください。

あるいは、以下の Notification コントローラーを参照してください。

Dev UI Alert
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 Code Block
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>

モード: - properties - js - java - xml - json - yaml - sql - html - css - sass - markdown

内部コンポーネント

ユーザーの IDE (IDE を検出できる場合) で開くことができるリソース (Java ソースファイルなど) へのリンクを作成します。

import 'qui-ide-link';
<qui-ide-link title='Source full class name'
                        class='text-source'
                        fileName='${sourceClassNameFull}'
                        lineNumber='${sourceLineNumber}'>[${sourceClassNameFull}]</qui-ide-link>;
内部コントローラーの使用

特定のことを簡単にするために、いくつかの 内部コントローラー が用意されている:

Notifier

これは容易にトーストメッセージを表示できる方法です。トーストは画面上に配置でき (デフォルトでは左下)、レベル (Info、Success、Warning、Error) を設定できます。いずれかのレベルを primary にすることで、目を引くトーストメッセージを作成できます。

このコントローラーのソースについては、 こちら を参照してください。

使用例:

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

有効な位置はすべて こちら で確認できます。

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 footer tab] を参照してください。

Dev UI Log control
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

ルーターは主に内部で使用されます。これは、SPA内の正しいページ/セクションにURLをルーティングするために、隠れて Vaadin Router を使用しています。これはナビゲーションを更新し、履歴(戻るボタン)を許可します。これはまた、複数のページを持つエクステンションで利用可能なサブメニューを作成します。

controller には、役立ちそうなメソッドが記載されています。

バックエンドとの通信

ランタイムクラスパスに対する JsonRPC

ランタイムデータ (前述の [Build time data] ではない) を取得またはストリーミングするか、ランタイムクラスパス (デプロイメントクラスパスの対照) に対してメソッドを実行できます。実行時のデータ取得には、ランタイムモジュールの Java 側と、Web コンポーネントでの使用という 2 つのパートがあります。

Java パート

このコードは、データを UI に表示できるようにする役割を担います。

デプロイメントモジュールのプロセッサーに JsonPRCService を登録する必要があります。

@BuildStep(onlyIf = IsDevelopment.class)(1)
JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {(2)
    return new JsonRPCProvidersBuildItem(CacheJsonRPCService.class);(3)
}
1 必ず開発モードでのみ実行してください。
2 JsonRPCProvidersBuildItem を生成または返します。
3 ランタイム・モジュールの中に、UIでデータを利用可能にするメソッドを含むクラスを定義する。

次に、ランタイムモジュールで JsonRPC サービスを作成します。このクラスは、Bean を明示的にスコープ指定しない限り、デフォルトでアプリケーションスコープの Bean になります。何かを返すすべてのパブリックメソッドは、Web コンポーネントの Javascript から呼び出すことができるようになります。

これらのメソッドで返されるオブジェクトは次のとおりです。

  • プリミティブまたは 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<JsonArray> ` を返すこともできます。
2 メソッド名 getAll は Javascript で使用できます。

Webcomponent (Javascript) パート

これで、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 が返されます。

呼び出されるメソッドにパラメーターを渡すこともできます。以下はその例です (ランタイム 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 と呼ばれるパラメーターを 1 つ受け取ります。

Webcomponent (Javascript) の場合:

_clear(name) {
    this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { (1)
        this._updateCache(jsonRpcResponse.result)
    });
}
1 name パラメーターが渡されます。

ストリーミングデータ

データを継続的に画面にストリーミングすることで、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 jokes をストリーミングする 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

場合によっては、デプロイメントクラスパスに対してメソッドを実行したり、データを取得したりする必要があるかもしれません。同様の状況は JsonRPC 通信でも発生しますが、その場合はランタイムモジュールに 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 は、エクステンションの namespace で自動的にスコープ指定されます。
3 ここで、request-response メソッドと同じアクションを追加します。メソッド名 (json-rpc サービスと同じ方法で js から呼び出し可能) は generateManifests です。

アクションとして CompletableFuture/CompletionStage を返すこともできます。データをストリーミングする場合は、 addAction ではなく addSubscription を使用して Flow.Publisher を返す必要があります。

Dev UI Log

999-SNAPSHOT バージョンを使用してローカルアプリケーションを実行すると、Dev UI のフッターに Dev UI Log が表示されます。これは、ブラウザーと Quarkus アプリ間でやり取りされるすべての JSON RPC メッセージをデバッグする場合に役立ちます。

Dev UI Json RPC Log

ホットリロード

ホットリロードの発生時に、画面を自動的に更新できます。そのためには、Webcomponent が拡張する LitElementQwcHotReloadElement に置き換えます。

QwcHotReloadElementLitElement を拡張するため、コンポーネントは変わらず Lit Element です。

QwcHotReloadElement を拡張する場合は、 hotReload メソッドを使用する必要があります (Lit の render メソッドも提供する必要があります)。

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

// ...

export class QwcMyExtensionPage extends QwcHotReloadElement {

    render(){
        // ...
    }

    hotReload(){
        // ..
    }

}

カスタムカード

デフォルトの内蔵カードを使用したくない場合は、拡張ページに表示されるカードをカスタマイズすることができます。

これを行うには、提供されたカードの代わりに読み込まれる Web コンポーネントを提供し、これを 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}
}

状態 (事前)

状態を使用することで、プロパティーに状態を含めることができ、グローバルに再利用できます。状態プロパティーの例としては、テーマ、接続状態 (バックエンドに接続している場合) などがあります。

現在の組み込み 状態オブジェクトを参照してください。

Dev UI の状態は LitState を使用します。詳細は、 ドキュメント を参照してください。

エクステンションは、カードとページ以外にも、フッターにタブを追加できます。これは、継続的に発生する事柄に便利です。ページから移動すると、ページはバックエンドへの接続を失いますが、フッターのログは永続的に接続されたままになります。

フッターに何かを追加する方法はカードを追加する方法と同じですが、 CardPageBuildItem ではなく FooterPageBuildItem を使用する点のみ異なります。

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 では、ログを UI にストリーミングできます。

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

    // ....
}

上記で説明したフッターを作成せずに、ログストリームをフッターに追加できる簡単な方法があります。 ログをタブにストリーミングする必要があるだけであれば、 FooterLogBuildItem を作成する以外の操作は必要ありません。この方法では、ログの名前と Flow.Publisher<String> のみを指定します。

以下は、Dev Services デプロイメントモジュールの例です。

@BuildStep(onlyIf = { IsDevelopment.class })
public List<DevServiceDescriptionBuildItem> config(
        // ...
        BuildProducer<FooterLogBuildItem> footerLogProducer){

        // ...

    // Dev UI Log stream
    for (DevServiceDescriptionBuildItem service : serviceDescriptions) {
        if (service.getContainerInfo() != null) {
            footerLogProducer.produce(new FooterLogBuildItem(service.getName(), () -> {
                return createLogPublisher(service.getContainerInfo().getId());
            }));
        }
    }
}

// ...

private Flow.Publisher<String> createLogPublisher(String containerId) {
    try (FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback()) {
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
        resultCallback.addConsumer(OutputFrame.OutputType.STDERR,
                frame -> publisher.submit(frame.getUtf8String()));
        resultCallback.addConsumer(OutputFrame.OutputType.STDOUT,
                frame -> publisher.submit(frame.getUtf8String()));
        LogContainerCmd logCmd = DockerClientFactory.lazyClient()
                .logContainerCmd(containerId)
                .withFollowStream(true)
                .withTailAll()
                .withStdErr(true)
                .withStdOut(true);
        logCmd.exec(resultCallback);

        return publisher;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

セクションメニューの追加

これにより、エクステンションはセクションメニュー内のページに直接リンクできるようになります。

セクションメニューに何かを追加する方法はカードを追加する方法と同じですが、 CardPageBuildItem ではなく MenuPageBuildItem を使用する点のみ異なります。

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

ページはカードに似た任意のページにできます。

テスト

エクステンションには、以下をテストするテストを追加できます。

  • ビルド時データ

  • JsonRPC経由の実行時データ

これを pom に追加する必要があります。

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

そうすることで、テストを作成するための 2 つのベースクラスにアクセスできるようになります。

ビルド時データのテスト

ビルド時データを追加した場合の例:

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
    }

}

実行時データのテスト

実行時データレスポンスが含まれる 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);
    }
}

関連コンテンツ