#author("2024-01-09T12:38:25+09:00","","")
#author("2024-01-09T12:40:52+09:00","","")
#mynavi(Grafana);
#setlinebreak(on);

* 概要 [#ga94c522]
#html(<div class="pl10">)
ここでは @grafana/create-plugin を使用して Grafana のパネルプラグインの開発手順を記載する。
また、以前に @grafana/toolkit ツールで作成されたプラグインは create-plugin で移行する事が可能(  [[移行ガイド>https://grafana.com/developers/plugin-tools/get-started/migrate-from-toolkit]] )。
#html(</div>)

* 目次 [#s4a88cd0]
#contents
- 関連
-- [[Grafana]]
-- [[Grafanaプラグイン開発(grafana/toolkit版)]]
-- [[Grafanaバックエンドデータソースプラグイン開発]]
- 参考
-- https://grafana.com/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/develop-a-plugin/build-a-panel-plugin/

//-------------------------------------------------------------------------

// START 開発環境の構築
* 開発環境の構築 [#q1e33f83]
#html(<div class="pl10">)

[[開発者ガイド>https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md]]に従って以下をセットアップする。

- Git
- Go
- Node.js
- Yarn

Mac
#myterm2(){{
brew install git
brew install go
brew install node@18
npm install -g yarn
}}

Windows
#html(<div class="pl10">)
https://grafana.com/blog/2021/03/03/how-to-set-up-a-grafana-development-environment-on-a-windows-pc-using-wsl/
#html(</div>)

#html(</div>)
// END 開発環境の構築

//-------------------------------------------------------------------------

* 雛形の作成 [#lb9d5407]
// START 雛形の作成
#html(<div class="pl10">)

ここでは sample1 という名前で パネルプラグインを作成した。
#myterm2(){{
npx @grafana/create-plugin@latest
Need to install the following packages:
  @grafana/create-plugin@1.10.1
Ok to proceed? (y) y
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
? What is going to be the name of your plugin? sample1
? What is the organization name of your plugin? my
? How would you describe your plugin? sample panel
? What type of plugin would you like? panel
? Do you want to add Github CI and Release workflows? No
? Do you want to add a Github workflow for automatically checking "Grafana API compatibility" on PRs? No
&#10004;  ++ /Users/xxxxxxx/Desktop/Iot/develop-grafana-plugin/my-sample1-panel/README.md
&#10004;  ++ /Users/xxxxxxx/Desktop/Iot/develop-grafana-plugin/my-sample1-panel/src/components/SimplePanel.tsx
    :
&#10004;  updateGoSdkAndModules
&#10004;  printSuccessMessage Congratulations on scaffolding a Grafana panel plugin! &#128640;

## What's next?

Run the following commands to get started:

    * cd ./my-sample1-panel
    * npm install to install frontend dependencies.
    * npm run dev to build (and watch) the plugin frontend code.
    * docker-compose up to start a grafana development server. Restart this command after each time you run mage to run your new backend code.
    * Open http://localhost:3000 in your browser to create a dashboard to begin developing your plugin.

Note: We strongly recommend creating a new Git repository by running git init in ./my-sample1-panel before continuing.

    * View create-plugin documentation at https://grafana.github.io/plugin-tools/
    * Learn more about Grafana Plugins at https://grafana.com/docs/grafana/latest/plugins/developing/development/


npm notice
npm notice New minor version of npm available! 9.5.1 -> 9.8.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.8.1
npm notice Run npm install -g npm@9.8.1 to update!
npm notice
}}

#html(</div>)
// END 雛形の作成

//-------------------------------------------------------------------------

* 生成されるソース [#i001a2de]
// START 生成されるソース
#html(<div class="pl10">)

grafana/create-plugin の場合は grafana/toolkit と違い、ビルド時に grafana/create-plugin は経由せずに直接 webpack、tsc を用いてビルドされる。
grafana/create-plugin が行うのは、あくまでも各種設定ファイルや初期テンプレートの生成(scaffold)のみである為、
生成済みのソースに対するビルド自体は grafana/create-plugin が導入されていない環境でも可能となっている。

尚、生成(scaffold)されるソースは以下の通り。
#html(){{
<pre>
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cypress
├── cypress.json
├── dist
├── docker-compose.yaml
├── jest-setup.js
├── jest.config.js
├── node_modules
├── package-lock.json
├── package.json
├── provisioning
├── src
|       ├── README.md
|       ├── components
|       │&#160;&#160; └── SimplePanel.tsx (パネルコンポーネント) ※react の関数コンポーネント
|       ├── img
|       │&#160;&#160; └── logo.svg
|       ├── module.ts  (パネルオプションの定義)
|       ├── plugin.json
|       └── types.ts.   (各パネルオプションのデータ型の定義)
└── tsconfig.json
</pre>
}}

#html(</div>)
// END 生成されるソース

//-------------------------------------------------------------------------

* そのままビルド/動作確認してみる [#i2684f7d]
// START そのままビルド/動作確認してみる
#html(<div class="pl10">)

各ソースの解説は後に行う事とし、まずはそのままビルドして動かしてみる。

** 対象バージョンの設定 [#ie3619a4]
// START 対象バージョンの設定
#html(<div class="pl10">)
docker-compose の Grafanaバージョン等を対象のバージョンに書き換える。(または 環境変数 GRAFANA_IMAGE、GRAFANA_VERSION を設定する)
#mycode2(){{
        grafana_image: ${GRAFANA_IMAGE:-grafana-oss}
        grafana_version: ${GRAFANA_VERSION:-8.5.27-ubuntu}
}}
#html(</div>)
// END 対象バージョンの設定

** ビルド [#n396b07c]
// START ビルド
#html(<div class="pl10">)
#myterm2(){{
cd my-sample1-panel
npm install
npm run build
}}
#html(</div>)
// END ビルド

** 起動 [#j70ed039]
// START 起動
#html(<div class="pl10">)
#myterm2(){{
# docker-compose-plugin を使用している場合は、「docker compose up」
docker-compose up
}}
#html(</div>)
// END 起動

** サンプルダッシュボード作成 [#o87060f1]
// START サンプルダッシュボード作成
#html(<div class="pl10">)

http://localhost:3000 にアクセスして ダッシュボード作成し、いま作成したパネルプラグインを動かしてみる。

#html(<div>)
#html(<div style="display: inline-block; vertical-align: top">)
&ref(create-dashboard1.png);
#html(</div>)
#html(<div style="display: inline-block; vertical-align: top; font-size: 1.5rem; font-weight: bold; padding-top: 20px;">→</div>)
#html(<div style="display: inline-block; vertical-align: top">)
&ref(create-dashboard2.png);
#html(</div>)
#html(<div style="display: inline-block; vertical-align: top; font-size: 1.5rem; font-weight: bold; padding-top: 20px;">→</div>)
#html(<div style="display: inline-block; vertical-align: top">)
&ref(create-dashboard3.png);
#html(</div>)
#html(<div style="display: inline-block; vertical-align: top; font-size: 1.5rem; font-weight: bold; padding-top: 20px;">→</div>)
#html(<div style="display: inline-block; vertical-align: top">)
&ref(create-dashboard4.png);
#html(</div>)
#html(<div style="display: inline-block; vertical-align: top; font-size: 1.5rem; font-weight: bold; padding-top: 20px;">→</div>)
#html(<div style="display: inline-block; vertical-align: top">)
&ref(create-dashboard5.png);
#html(</div>)
#html(</div>)

#html(</div>)
// END サンプルダッシュボード作成

#html(</div>)
// END そのままビルド/動作確認してみる

//-------------------------------------------------------------------------

* コンポーネント引数の概要 [#m892e84e]
// START コンポーネント引数の概要
#html(<div>)

パネル本体のソースは SimplePanel.tsx として生成されており、React の関数コンポーネントとなっている。
関数コンポーネントのプロパティは @grafana/data の PanelProps を継承しており、クエリデータやパネルオプション、幅や高さなど様々な情報が取得できる。

生成されたソースにデフォルトで展開される引数は以下の4つだけだが、PanelProps には他にもいくつかのプロパティが定義されており、必要に応じて利用できる。
※パネルタイトル名を指す title や 他パネルとの連携用の eventBus、Variables値の取得用の replaceVariables など。
 ( https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/panel.ts#L74C23-L74C23 )

#html(<div style="display: inline-block; vertical-align: top; padding-right: 20px;">)

| 引数 | 説明 |h
| options | パネルオプションの情報(後述) |
| data      | データ(後述) |
| width    | パネルの幅 |
| height  | パネルの高さ |


#html(</div>)
#html(<div style="display: inline-block; vertical-align: top;">)

#mycode2(){{
import React from 'react';     
import { PanelProps } from '@grafana/data';
import { SimpleOptions } from 'types'; 
import { css, cx } from '@emotion/css';
import { useStyles2, useTheme2 } from '@grafana/ui'; 
    :
interface Props extends PanelProps<SimpleOptions> {}
    :                                                                   // ↓ 
export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
  const theme = useTheme2();
  const styles = useStyles2(getStyles); 
  return (
    <div
      className={cx(
     :
}}
#html(</div>)

#html(</div>)
// END コンポーネント引数の概要

//-------------------------------------------------------------------------

* コンポーネント引数(data) [#v5c46053]
// START コンポーネント引数
#html(<div class="pl10">)

コンポーネント引数の data からはクエリデータや読み込み状況などが取得できる。

以下に取得できる主な値を記載する。

| データ | 説明 | 補足 |h
| data.state | パネルデータの読み込み状況 | NotStarted, Loading, Streaming, Done, Error のいずれか |
| data.request | 要求データ | 日付やクエリ条件 |
| data.timeRange | 日付条件 | 日付条件 |
| data.series | クエリの検索結果(詳細は後述) | 各クエリで取得されたデータ |

** data.state [#fdcbf8d0]
#html(<div class="pl10">)
パネルの読み込み状態を指す文字列が返される。(NotStarted, Loading, Streaming, Done, Error のいずれか(※)
クエリ結果の画面への反映は基本的に 'Done' 時に行う。
※ https://github.com/grafana/grafana/blob/08bf2a54523526a7f59f7c6a8dafaace79ab87db/packages/grafana-data/src/types/data.ts#L3
#html(</div>)

** data.request [#ac02aa6d]
#html(<div class="pl10">)
API呼び出し時の要求データが返される。
内容には対象のクエリや埋め込み用パラメータ、日時などが含まれる。

data.requestのサンプル
#mycode2(){{
{
    "app": "dashboard",
    "requestId": "Q100",
    "timezone": "browser",
    "panelId": 2,
    "dashboardId": 1,
    "range": {
        "from": "2023-04-01T02:03:44.323Z",
        "to": "2023-04-01T08:03:44.323Z",
        "raw": {"from": "now-6h", "to": "now"}
    },
    "timeInfo": "",
    "interval": "15s",
    "intervalMs": 15000,
    "targets": [
        {
            "refId": "A",
            "datasource": {
                "type": "datasource",
                "uid": "grafana"
            },
            "queryType": "randomWalk"
        }
    ],
    "maxDataPoints": 1432,
    "scopedVars": {
        "__interval": {
            "text": "15s",
            "value": "15s"
        },
        "__interval_ms": {
            "text": "15000",
            "value": 15000
        }
    },
    "startTime": 1704528224323,
    "rangeRaw": {
        "from": "now-6h",
        "to": "now"
    },
    "endTime": 1704528224331
}
}}

#html(</div>)

** data.timeRange [#u2535916]
#html(<div class="pl10">)

data.timeRangeのサンプル
#mycode2(){{
{
    "from": "2023-04-01T02:07:08.268Z",
    "to": "2023-04-01T08:07:08.268Z",
    "raw": {
        "from": "now-6h",
        "to": "now"
    }
}
}}

#html(</div>)

** data.series [#sec6fdbe]
#html(<div class="pl10">)

- クエリ結果(data.series)はデータフレームとして返却される。
-- データフレームの概念自体はpython の pandas や R 等と変わらないが、Grafana 独自の形式になっている。
https://grafana.com/developers/plugin-tools/create-a-plugin/develop-a-plugin/work-with-data-frames
https://grafana.com/developers/plugin-tools/introduction/data-frames
- data.series は配列になっておりクエリの数だけ返却される可能性がある。(パネルに設定したクエリが1つの場合は data.series は1件)
-- データがどのクエリの結果かは refId で判断する。

data.series のサンプル
#mycode2(){{
[
    {
        "refId": "A",
        "fields": [
            {
                "name": "time",
                "type": "time",
                "typeInfo": {"frame": "time.Time", "nullable": true},
                "config": {
                    "interval": 15000,
                    "thresholds": { "mode": "absolute", "steps": [{"value": null, "color": "green"}, {"value": 80, "color": "red"}] },
                    "color": {"mode": "thresholds"}
                },
                "values": [
                    1704507239849,
                    1704507254849,
                    1704507269849,
                    1704507284849,
                    1704528299849
                ],
                "entities": {},
                "state": {
                    "scopedVars": { "__series": { "text": "Series", "value": {"name": "Series (A)"} }, "__field": { "text": "Field", "value": {} } },
                    "seriesIndex": 0
                }
            },
            {
                "name": "A-series",
                "type": "number",
                "typeInfo": {"frame": "float64", "nullable": true},
                "labels": {},
                "config": {
                    "mappings": [],
                    "thresholds": { "mode": "absolute", "steps": [{"value": null, "color": "green"}, {"value": 80, "color": "red" }] },
                    "color": {"mode": "thresholds"}
                },
                "values": [
                    21.39141804533429,
                    21.269982656049816,
                    21.409325409660905,
                    21.27445038763838,
                    21.571137801382193
                ],
                "entities": {},
                "state": {
                    "scopedVars": { "__series": {"text": "Series", "value": {"name": "Series (A)"} }, "__field": {"text": "Field", "value": {} } },
                    "seriesIndex": 0,
                    "range": {"min": 20.043626241840062, "max": 33.74705135437393, "delta": 13.703425112533868}
                }
            }
        ],
        "length": 5
    }
]
}}

** 標準オプション設定を有効にしているパネルの場合の注意 [#ka1c8531]
#html(<div class="pl10">)

標準オプション設定(※1)を有効にしているパネルの場合、データフレームの値をそのまま画面に表示してしまうと、オプション設定が反映されていない素の値が表示されてしまう。
その為、データフレームの field には項目設定を反映した値を得る為の display メソッドが用意されている(※2)。
display メソッドの戻り値の text、 prefix、suffix を組み合わせる事でオプション設定を反映した値を得る事ができる。(※3)
※小数点以下の桁数(Decimal)、単位(Unit)、Value Mappings 等。

※1 オプションについては後述
※2 https://grafana.com/developers/plugin-tools/create-a-plugin/develop-a-plugin/work-with-data-frames#display-values-from-a-data-frame
※3 独自の項目オプション設定を追加している場合は、当然 display を使用しても自動的には反映されないので、設定内容に応じて値を自分で値を編集する必要がある。

以下に display の使用例を記載する。
#mycode2(){{
import React, { useState } from 'react';
import { PanelProps } from '@grafana/data';
import { SimpleOptions } from 'types';

interface Props extends PanelProps<SimpleOptions> {}

export const SampleTable: React.FC<Props> = ({ options, data, width, height }) => {

  // データ取得日時
  const [lastTime, setLastTime] = useState(0);
  // 表示用データ
  const [rows, setRows] = useState<any>([]);
  // 項目名リスト
  const [columns, setColumns] = useState<string[]>([]);

  const getRows = (frame: any) => {
    let rows: any = [];
    let columns: string[] = [];
    frame.fields.map((f: any) => {
      const name = f.name;
      columns.push(name);
      f.values.map((v: any, i: number) => {
        let row: any = {};
        if (i < rows.length) {
          row = rows[i]
        } else {
          rows.push({});
        }
        // 素の値は使用しない
        // row[name] = v;

        // display メソッドで得られたテキスト値に、接頭/末尾文字を付加したものを表示する。
        const dispValue = f.display!(v);
        const prefix = dispValue.prefix || '';
        const suffix = dispValue.suffix || '';
        row[name] = prefix + dispValue.text + suffix;

        rows[i] = row;
      });
    });
    return [columns, rows];
  };

  if (data.state === "Done" && data?.request?.endTime !== lastTime) {
    setLastTime(data?.request?.endTime || 0);
    const result = getRows(data.series[0]);
    setColumns(result[0]);
    setRows(result[1]);
  }

  return (
    <div style={ {height: height, overflowY: 'scroll'} }>
      <div>count: {rows ? rows.length : 0}</div>
      <div>start</div>
      <table style=&#123;{border: '1px solud #fff', borderCollapse: 'collapse'}&#125;>
      {rows
        ? rows.map((row: any, i: number) => {
            const data = columns.map((name) => {
                return <td key={`{name}_{i}`} style=&#123;{border: '1px solid #fff', padding: 4}&#125;>{row[name]}</td>
            })
            return <tr key={`row_{i}`}>{data}</tr>
          })
        : null}
      </table>
      <div>end</div>
    </div>
  );
};
}}
#html(</div>)
// END 標準オプション設定を有効にしているパネルの場合の注意

#html(</div>)
// END data.series

#html(</div>)
// END コンポーネント引数

//-------------------------------------------------------------------------

* パネルから Variables を取得/変更する [#nc85f7e0]
// START パネルから Variables を取得/変更する
#html(<div class="pl10">)

** Variables値の取得 [#efb1026e]
コンポーネント引数から取得した replaceVariables 関数を利用する
例)
#mycode(){{
// replaceVariables の引数に指定した文字列の "$variable名" の部分が実際のVariableの値に置換される
let value = replaceVariables('$text1')
console.log('text1 の値は ' + value + ' です');
}}

** Variables値の変更 [#l1979ddb]
@grafana/runtime の locationService を利用する。
https://github.com/grafana/grafana/blob/main/packages/grafana-runtime/src/services/LocationService.ts

例)
#mycode(){{
// partial メソッドで一部の Variablesの変更が可能。
// variablesのURLパラメータ名の接頭文字には  "var-" が付与される為、注意。
locationService.partial({'var-text1': '変更後の値'}, true)
}}

サンプル
#mycode2(){{
import React, { useRef } from 'react';
import { PanelProps } from '@grafana/data';
import { Button, InlineField, Input } from '@grafana/ui';
import { locationService } from '@grafana/runtime';
import { SimpleOptions } from 'types';

interface Props extends PanelProps<SimpleOptions> {}

export const SampleChangeVariables: React.FC<Props> = ({ options, data, width, height, replaceVariables }) => {

  const refName = useRef<HTMLInputElement>(null);
  const refValue = useRef<HTMLInputElement>(null);

  const getVariables = (varName: string) => {
    return replaceVariables('$' + varName) || '';
  };

  const updateVariables = (varName: string, value: any) => {
    let data: any = {};
    data['var-' + varName] = value;
    locationService.partial(data, true);
  };

  return (
    <div>
      <div>
        <InlineField label="Variablesの名前">
          <Input ref={refName} name="targetName" />
        </InlineField>
        <InlineField label="Variablesの値">
          <Input ref={refValue} name="targetValue" />
        </InlineField>
      </div>
      <Button
        size="md" variant="success" fill="outline" tooltipPlacement="top"
        onClick={()=>{
          let targetName = refName?.current?.value;
          if (targetName) {
            refValue!.current!.value = getVariables(targetName);
          }
        &#125;&#125;>
        <div>Variablesを取得</div>
      </Button>
      <Button
        size="md" variant="success" fill="outline" tooltipPlacement="top"
        onClick={()=>{
          let targetName = refName?.current?.value;
          if (targetName) {
            updateVariables(targetName, refValue?.current?.value);
          }
        &#125;&#125; >
        <div>Variablesを変更</div>
      </Button>
    </div>
  );
};
}}

#html(</div>)
// END パネルから Variables を取得/変更する

//-------------------------------------------------------------------------

* プラグインから任意のSQLを発行する[#u9d42ce6]
// START プラグインから任意のSQLを発行する
#html(<div class="pl10">)

@grafana/runtime の getBackendSrv を利用すればプラグイン処理から直接SQLの発行(APIリクエスト)を行う事ができる。
APIリクエスト時にはGrafanaに登録されているデータソース名が必要(※)。
※厳密には必要なのはデータソースIDだが、データソース名さえわかっていればconfig から取得可能。
※つまりGrafanaのデータソース設定に登録されていない任意のDBなどに接続できるわけではない。

サンプル
#mycode2(){{
import React, { useState } from 'react';
import { PanelProps, toDataFrame } from '@grafana/data';
import { Button } from '@grafana/ui';
import { config, getBackendSrv } from '@grafana/runtime';
import { SimpleOptions } from 'types';

interface Props extends PanelProps<SimpleOptions> {}

export const SampleCallApi: React.FC<Props> = ({ options, data, width, height, replaceVariables }) => {

  // 項目名リスト
  const [fieldNames, setFieldNames] = useState<string[]>([]);

  // 表示データ
  const [rows, setRows] = useState<any[]>([]);

  // SQLの発行先のデータソース名
  const dsName = 'sampledb';

  // バックエンドAPI呼び出し
  const callApiTest = () => {

    // クエリID(多分パネルに設定するIDと被りさえしなければ良い)
    const queryId = 'custom_query';

    // データソースリクエストに必要なデータソースIDはconfigから取得する
    const dsId = config.datasources[dsName]['id'];

    // 発行するSQL
    const rawSql = 'select * from books order by id';

    let data = {
      queries: [
        {
          refId: queryId,
          rawSql: rawSql,
          format: 'table',
          datasourceId: dsId,
        }
      ],
    };
    getBackendSrv().datasourceRequest({
      method: 'POST',
      url: '/api/ds/query',
      headers: {'Content-Type': 'application/json'},
      data: JSON.stringify(data),
    })
    .then((response) => {
      if (response.ok) {
        const frame = toDataFrame(response.data.results[queryId].frames[0]);
        const fieldNames = frame.fields.map((x: any)=>x.name);
        let rows: any = [];
        frame.fields.map((f: any)=>{
          let name = f.name;
          f.values.map((v: any,i: number)=>{
            let row: any = {};
            if (i >= rows.length) {
              rows.push({})
            } else {
              row = rows[i];
            }
            row[name] = v;
            rows[i] = row;
          });
        })
        setFieldNames(fieldNames);
        setRows(rows);
      }
    });
  };

  return (
    <div>
      <Button
        size="md" variant="success" fill="outline" tooltipPlacement="top"
        onClick={()=>callApiTest()} >
        <div>SQL発行</div>
      </Button>
      <table>
        <thead>
        <tr>
        {fieldNames ? fieldNames.map((field: string, i: number)=>{
          return <th key={`header_{i}`}>{field}</th>
        }) : null}
        </tr>
        </thead>
        <tbody>
        {rows ? rows.map((row: any, i: number)=>{
          let rowHtml = fieldNames.map((field: string)=>{
            return <td key={`{field}_{i}`}>{row[field]}</td>
          });
          return <tr key={`row_{i}`}>{rowHtml}</tr>
        }) : null}
        </tbody>
      </table>
    </div>
  );
};
}}


#html(</div>)
// END プラグインから任意のSQLを発行する

//-------------------------------------------------------------------------

* パネルにオプション設定を付加する [#n4844b7f]
// START パネルにオプション設定を付加する
#html(<div class="pl10">)

module.ts を編集する事によって様々なプラグインオプションを設置する事ができる。
※オプション設定の情報はコンポーネント引数 options から取得可能。

- 項目レベルのオプション設定
-- useFieldConfig でクエリ結果の項目レベルの表示オプションを設置する事ができる。
-- 項目レベルのオプション設定は共通設定 及び 項目単位のOverride設定が可能となる。
-- 標準の項目オプションは disableStandardOptions で指定しない限り自動で追加される。
-- 独自のオプションは useCustomConfig で各種ビルダーを使用して設置する。

- パネル単位のオプション設定
-- setPanelOptions でパネル単位のオプションを設置する事ができる

どちらもメソッドチェインして複数のビルダーを定義する事が可能。
※ビルダーについては後述

例)
#mycode2(){{
return builder
  .addTextInput({
      :
  })
  .addNumberInput({
      :
  })
}}

module.ts
#mycode2(){{
import { FieldConfigProperty , PanelPlugin } from '@grafana/data';
import { SimpleOptions } from './types';
import { SimplePanel } from './components/SimplePanel';

export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel)
  // 項目レベルのオプション設定
  .useFieldConfig({
    disableStandardOptions: [FieldConfigProperty.Thresholds],       // 使用しない標準オプションはここで無効化できる
    useCustomConfig: (builder) => {
    return builder
      .addBooleanSwitch({
        path: 'fieldOption1',
        name: 'field option boolean',
        defaultValue: false,
      })  
    },  
  }) 
  // パネル単位のオプション
  .setPanelOptions((builder) => {
  return builder
    .addTextInput({
      path: 'sampleText',
      name: 'Simple text option',
      description: 'Description of panel option',
      defaultValue: 'test',
    });
});
}}

** 標準のオプション設定(StandardOptions) [#o8fd9f70]
#html(<div>)

useFieldConfig を使用した場合は、標準の項目オプションが(※)自動的に追加される。
※ https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/fieldOverrides.ts#L142

#html(){{
・項目オプションを標準のまま使用する場合は、引数なしで <span style="display: inline-block; background: #eee; padding: 0 4px;">.useFieldConfig({})</span> と宣言すれば良い。<br />
・useFieldConfig 自体を書かない場合は、標準の項目オプションは追加されない。<br />
・また、使用したくない標準オプションがある場合は disableStandardOptions に対象のオプションを指定する事で非表示にできる。<br />
}}

例) 標準の項目オプションを使用するが、閾値設定、フィルタ設定のオプションのみ表示しない場合
#mycode2(){{
import { FieldConfigProperty , PanelPlugin } from '@grafana/data';
import { SimpleOptions } from './types';
import { SimplePanel } from './components/SimplePanel';

export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel)
  .useFieldConfig({
    disableStandardOptions: [FieldConfigProperty.Thresholds, FieldConfigProperty.Filterable],
  })
});
}}

#html(</div>)

** 使用できるオプションUIビルダー [#n4844b7f]
#html(<div class="pl10">)

項目オプション、パネルオプションにカスタム設定を追加する際に利用できるUIビルダーは、ここら辺を見ればおおよそ把握出来る。
https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/utils/OptionsUIBuilders.ts

| ビルダ | 説明 |h
| addNumberInput | 数値用 |
| addSliderInput | スライダー型 |
| addTextInput | テキスト入力用 |
| addStringArray | 文字列配列 |
| addSelect | プルダウン選択用 |
| addRadio | Radio選択用 |
| addBooleanSwitch | Bool値用 |
| addUnitPicker | 単位選択用 |
| addColorPicker | 色選択用 |
 
#html(){{
<div id="tabs1">
  <ul>
    <li><a href="#tabs1-1">設定画面の例</a></li>
    <li><a href="#tabs1-2">src/module.ts</a></li>
    <li><a href="#tabs1-3">src/types.ts</a></li>
    <li><a href="#tabs1-4">src/components/SimplePanel.tsx</a></li>
  </ul>
}}

// START tabs1-1
#html(<div id="tabs1-1">)
&ref(sample_options.png); 
#html(</div>)
// END tabs1-1

// START tabs1-2
#html(<div id="tabs1-2">)

#mycode2(){{
import { PanelPlugin } from '@grafana/data';
import { SimpleOptions } from './types';
import { SimplePanel } from './components/SimplePanel';
      
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOptions((builder) => {
  return builder
    .addTextInput({
      path: 'sampleText',
      name: 'Simple text option',
      description: 'Description of panel option',
      defaultValue: 'test',
    })
    .addNumberInput({
      path: 'sampleNumber',
      name: 'Simple number option',
      description: '数値のオプション設定です',
      defaultValue: 10,
    })    
    .addStringArray({
      path: 'sampleStrings',
      name: 'String Array option',
      description: '文字列配列のオプション設定です',
      defaultValue: ['test1', 'test2'],
    })
    .addSelect({
      path: 'sampleSelect',
      defaultValue: 'value1',
      name: 'sample select',
      settings: {
        options: [
          {label: 'value1', value: 'Value1'},
          {label: 'value2', value: 'Value2'},
          {label: 'value3', value: 'Value3'},
        ]
      },  
    })    
    .addBooleanSwitch({
      path: 'sampleBoolean',
      name: 'sample boolean',
      defaultValue: false,
    })
    .addRadio({
      path: 'sampleRadio',
      name: 'sample radio',
      defaultValue: 'value1',
      settings: {
        options: [
          { value: 'value1', label: 'Value1'},
          { value: 'value2', label: 'Value2'},
          { value: 'value3', label: 'Value3'},
        ],
      },
      showIf: (config) => config.sampleBoolean,
    })
    .addSliderInput({
      path: 'sampleSlider',
      name: 'sample slider',
      description: 'スライダー型のオプション設定です',
      defaultValue: 0,
      settings: { min: 0, max: 100, step: 1},
    })
    .addUnitPicker({
      path: 'sampleUnit',
      name: 'sample unit',
    })
    .addColorPicker({
      path: 'sampleColor',
      name: 'sample color',
      defaultValue: '#ff0000',
    });
});
}}
#html(</div>)
// END tabs1-2

// START tabs1-3
#html(<div id="tabs1-3">)
#mycode2(){{
type SampleValues= 'value1' | 'value2' | 'value2';

export interface SimpleOptions {
  sampleText: string;
  sampleNumber: number;
  sampleStrings: [string];
  sampleSelect: string;
  sampleBoolean: boolean;
  sampleRadio: SampleValues;
  sampleSlider: number;
  sampleUnit: string;
  sampleColor: string;
}
}}
#html(</div>)
// END tabs1-3

// START tabs1-4
#html(<div id="tabs1-4">)

実際のオプション設定値は引数 options から取得可能。

#mycode2(){{
import React from 'react';     
import { PanelProps } from '@grafana/data';
import { SimpleOptions } from 'types'; 

interface Props extends PanelProps<SimpleOptions> {}

export const SimplePanel: React.FC<Props> = ({ options, data, width, height }) => {
  return (
    <div>                      
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleText</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleText}</div></div>
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleNumber</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleNumber}</div></div>
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleSelect</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleSelect}</div></div>
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleStrings</div>:<div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleStrings.join(',')}</div></div>
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleBoolean</div>:<div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleBoolean ? 'true' : 'false'}</div></div> 
      {options.sampleBoolean && (     
        <div>options.sampleRadio: {options.sampleRadio}</div>
      )}
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleSlider</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleSlider}</div></div>
      <div><div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleUnit</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleUnit}</div></div>
      <div style=&#123;&#123;color: options.sampleColor&#125;&#125;>
        <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>options.sampleColor</div>: <div style=&#123;&#123;display: 'inline-block'&#125;&#125;>{options.sampleColor}</div>
      </div>                   
    </div>
  );
};
}}
#html(</div>)
// END tabs1-4

#html(</div>)
#html(<script>$(function() { $("#tabs1").tabs(); });</script>)
// END tabs1

** 独自のパネルオプション [#aa41f280]

また、addCustomEditor を使用して、独自のパネルオプションを構築する事も可能。
https://grafana.com/docs/grafana/latest/developers/plugins/create-a-grafana-plugin/extend-a-plugin/custom-panel-option-editors/

#html(</div>)
// END 使用できるオプションUIビルダー 

#html(</div>)
// END パネルにオプション設定を付加する

//-------------------------------------------------------------------------

* Grafana コンポーネントの利用 [#lc12166a]
// START Grafana コンポーネントの利用
#html(<div class="pl10">)

ボタンなどの各種UI部品は Grafana の UIコンポーネントを利用する事が出来る。

Storybook が用意されているので、これを参照して各種UIコンポーネントを独自パネルに組み込む。
https://developers.grafana.com/ui/latest/
※ただし使用中のGrafanaバージョンによっては、パラメータ等が異なる場合がある点は注意(結局ソースを見た方が早いケースもある)

#html(){{
<div id="tabs2">
  <ul>
    <li><a href="#tabs2-1">サンプル画面</a></li>
    <li><a href="#tabs2-2">パネルソース</a></li>
  </ul>
}}

// START tabs2-1
#html(<div id="tabs2-1">)

#html(<div class="pb10">)
&ref(sample_grafanaui1.png); 
#html(</div>)
#html(<div class="pb10">)
&ref(sample_grafanaui2.png); 
#html(</div>)
#html(<div class="pb10">)
&ref(sample_grafanaui3.png); 
#html(</div>)

#html(</div>)
// END tabs2-1

// START tabs2-2
#html(<div id="tabs2-2">)
#mycode2(){{
import React, &#123; useState &#125; from 'react';
import &#123; PanelProps &#125; from '@grafana/data';
import &#123; Alert, Button, ConfirmModal &#125; from '@grafana/ui';
import &#123; SimpleOptions &#125; from 'types';

interface Props extends PanelProps&lt;SimpleOptions&gt; &#123;&#125;

export const SampleGrafanaUI: React.FC&lt;Props&gt; = (&#123; options, data, width, height &#125;) =&gt; &#123;

  const [showModal, setShowModal ] = useState(false);
  const [message, setMessage] = useState&lt;any&gt;(&#123;&#125;);

  return (
    &lt;div&gt;
      &lt;Button
        size="md"
        variant="success"
        fill="outline"
        tooltipPlacement="top"
        onClick=&#123;()=&gt;&#123;
          setMessage(&#123;&#125;);
          setShowModal(!showModal);
        &#125;&#125; &gt;
        &lt;div&gt;クリックすると確認ダイアログを表示します&lt;/div&gt;
      &lt;/Button&gt;
      &lt;ConfirmModal
        isOpen=&#123;showModal&#125;
        title="サンプル確認ダイアログ"
        body="ボタンを押下してください。"
        confirmText="OK"
        onConfirm=&#123;() =&gt; &#123;
          setMessage(&#123;title: 'OK', severity: 'success', text: 'OKが押下されました'&#125;);
          setShowModal(false);
        &#125;&#125;
        onDismiss=&#123;() =&gt; &#123;
          setMessage(&#123;title: 'キャンセル', severity: 'info', text: 'キャンセルされました'&#125;);
          setShowModal(false);
        &#125;&#125;
      /&gt;
      &lt;div style=&#123;&#123;marginTop: 10&#125;&#125;&gt;
        &#123;message?.text
          ? (
              &lt;Alert
                title=&#123;message.title&#125;
                severity=&#123;message.severity&#125;
                buttonContent="X"
                onRemove=&#123;()=&gt;setMessage(&#123;&#125;)&#125;
                &gt;
                &#123;message.text&#125;
              &lt;/Alert&gt;
            )
          : null&#125;
      &lt;/div&gt;
    &lt;/div&gt;
  );
&#125;;
}}

#html(</div>)
// END tabs2-2

#html(</div>)
#html(<script>$(function() { $("#tabs2").tabs(); });</script>)
// END tabs2

#html(</div>)
// END Grafana コンポーネントの利用

//-------------------------------------------------------------------------

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS