在本章節中,我們將深入探討 Grafana Scenes 的概念。在上一部份中,我們已經熟悉一下 Grafana 的觀念,也認識了 Grafana Plugin 與 Scenes 之間的關聯,以及了解 Scene App 是透過結合多個資料來源的 Panel、Time range 和 Variable 來實現的。然而 Scene App 類型的 Plugin,無論是哪一層次或是哪一種 API,都是由 Scene Object 所構成的。因此,本章節將帶領讀者認識 Scenes 的核心——Scene Object,並逐步展示如何從初始化到完成一個 Hello World 的 Scene App。
Grafana Scenes 中有非常多種功能性或展示性的元件,例如組成整個 APP 的 SceneApp
、最通用的 Scene 元件 EmbeddedScene
,以及其他 SceneQueryRunner
, SceneFlexLayout
, VizPanel
等等,每一種都是由 Scene Object 延伸,也可以交互組合更支援嵌套巢狀,只要使用 children 屬性即可達成俄羅斯娃娃般的應用程式。
要了解 Scene Object 的原理,可以將其分為三個關鍵部分:State(狀態)、Model(模型)和 Render(渲染),他們形成了 Scene Object 的完整生命週期和功能。
interface DemoSceneState extends SceneObjectState {
counter: number;
}
State 代表 Scene Object 的當前狀態,包含所有可變的資料。在 Grafana Scene 中,狀態通常會延伸 SceneObjectState 的型別來定義和管理。
💡 SceneObjectState
是 Grafana Scene 中最基本的型別,會被所有 Object 的元件延伸擴展,可以自行添加屬性達到狀態管理需求,同時又擁有最基本的幾個狀態,讓在巢狀或複雜的結構中都可以調用狀態。其中預設狀態會以$
符號:$timeRange、$data、$variables 以及 $behaviors。
export class DemoScene extends SceneObjectBase<DemoSceneState> {
public static Component = DemoSceneRenderer;
public constructor(counter: number) {
super({ counter });
}
public onIncrement = () => {
this.setState({
counter: this.state.counter + 1,
});
};
}
Model 是一個類(Class)其中定義了 Scene Object 的結構、行為以及生命週期,通常會繼承 SceneObjectBase,主要封裝了狀態管理、業務邏輯和與其他 Scene Objects 的交互作用。
在實際應用中,Model 可能代表一個圖表、一個表格、一個 Panel、一個 Button,或者任何其他視覺化元件。通過組合不同的 Models,開發者可以創造複雜的、交互式的資料視覺化應用程式。以下有幾個常在 Model 中實現的功能:
提供更新狀態的方法,例如範例中的 onIncrement
調用 setState
進行狀態更新。
實現資料處理和商業邏輯
定義生命週期的一些 function(如 activate、deactivate)
public _onActivate() {
if (!this.state.mode) {
this.setState({ mode: 'service_total' });
}
return () => {
getUrlSyncManager().cleanUp(this);
};
}
處理使用者交互和事件:例如 SceneObjectBase 中有 subscribeToEvent 和 publishEvent,處理 Scene Objects 訂閱和發布事件。
定義與其他 Scene Objects 協調的 function:SceneObjectBase 中有 getRoot 和 forEachChild,可以讓 Scene Objects 與其父物件和子物件進行交互作用。
function DemoSceneRenderer({ model }: SceneComponentProps<DemoScene>) {
const { counter } = model.useState();
return <h1 onClick={model.onIncrement}>{counter}</h1>;
}
Render 在 Grafana Scene 中是將 Model 的狀態和邏輯轉換為視覺化 UI 元素的過程。會透過一個 Component
的靜態狀態實現,例如範例中的 DemoSceneRenderer
,而這個 Component 是一個 React 元件,接收 Scene Object 作為 prop,即為 model 物件。
Render 會以 model 的 useState hook 訂閱 Scene Object 的狀態變化,或是調用在 Model 中建立的 function,例如範例中的 model.onIncrement
。也可以以 model 本身這個實例作為 Grafana Scenes 提供 API 的參數使用。
const variables = sceneGraph.getVariables(model).useState()
若未使用 model.onIncrement
而是希望直接對狀態進行更新修改,則可以調用 model.setstate 方法實現:
const onIncrement = () => model.setState({ counter: counter + 1 })
在了解 Scene Object 的實現原理後,我們可以建立一個具體的範例,體驗 Scenes 的實際應用。範例中會從安裝到最後顯示一個最簡單的 Hello World 畫面的實作,其中會包括 Scene 建立時的初始化,例如會需要使用什麼類型的 Scene Object,以及需要設定什麼屬性。由於 Grafana 已經設計了一套實現應用程式可以使用的元件,因此如何選擇使用是非常重要的!
當完成第八章中的 Grafana Plugin 安裝,若選擇 Scene App,只要執行 yarn install
即可開始開發,因此建議直接選擇此類型安裝:
若安裝 Grafana Plugin 時所選擇的是 App 類型,則還需要另外安裝 Grafana Scene 才可以使用:
yarn add @grafana/scenes
NOTICE:
非常推薦先了解 Grafana Plugin 與 Grafana Scenes 之間的關係,再接下去安裝開發。
參考資料:靠 Grafana 吃飯的第八天 - Grafana 的百寶袋 - Plugin
安裝時選擇 Scene App 類型後,會建立一個含有四個路徑頁面的應用程式:Home、WithTabs、WithDrilldown 和 HelloWorld。主要開發皆會在 src 資料夾中,有 components、img、pages 和 utils 四個資料夾,以及集中常數的 constants、Plugin 應用程式初始設定的 module 檔案,最後是提供 Plugin 基本資料的 plugin.json 檔案。
Grafana Plugin 可以自由地新增具有路徑的頁面,可以在 components > Routes
中進行 React Router Dom 的路徑擴展設置,除此之外也必須在 plugin.json 檔中擴展 includes 屬性的內容:
{
"type": "page",
"name": "Hello world",
"path": "/a/%PLUGIN_ID%/hello-world",
"role": "Admin",
"addToNav": true,
"defaultNav": false
}
建立一個 Scene 有幾種搭配方法:
SceneApp 是應用程式的最頂層頁面,用於管理多個頁面和路由的頂層容器,通常會設置一個屬性 pages
,即為包下所有 SceneAppPage objects 的陣列:
const getScene = () => new SceneApp({
name: 'My Grafana App',
pages: [
// SceneAppPage 實例
],
});
SceneApp 的 Component 屬性即為一個路由元件,會取得 pages 中每個頁面的 url 進行管理,並以單一個 page SceneObject 作為實例屬性傳給 render 的 function,執行 React.createElement 以渲染:
<Switch>
{pages.map((page) => (
<Route
key={page.state.url}
exact={false}
path={page.state.url}
render={...}
></Route>
))}
</Switch>
SceneAppPage 代表 SceneApp 中的單個頁面。有著 SceneAppPageState 型別的狀態,包含了頁面的標題、副標題、URL(使用絕對路徑,i.e. /app/overview )以及該頁面的實例 getScene。另外也有提供特別的屬性:
renderTitle:可以渲染客製化的標題,但需要返回一個未加樣式的 <h1>
標籤,以及需要的任何其他元素。
hideFromBreadcrumbs:決定頁面是否應該在麵包屑路徑中顯示,例如下圖的麵包屑。
tabs:作為頁面頂部顯示的頁籤的 SceneAppPage
物件陣列,而標籤通常顯示在頁面頂部,使用者可以在相關內容之間快速切換。每個標籤都是一個 SceneAppPage
實例,可以擁有自己的標題、URL 和內容。
drilldown:讓使用者從高層次的圖表深入到更詳細的資訊。每個 drilldown 圖表定義了一個路由路徑(routePath)和一個取得頁面的 function(getPage),而這個路經通常為動態路徑,可以藉由 React Router 可以取得 URL 參數。另外 getPage 返回的是一個新的 SceneAppPage 實例,所以如果在每一層皆使用 drilldown 屬性,就可以無限巢狀深層頁面。使用場景為:
例如下圖中可以點擊進入不同 Route Details 的介面:
const getScene = () => new SceneApp({
name: 'My Grafana App',
pages: [
new SceneAppPage({
title: 'Page with tabs',
url: '/my-page/tabs',
hideFromBreadcrumbs: true,
getScene: getTab1Scene,
tabs: [
new SceneAppPage({
title: 'Tab 2',
url: '/my-page/tabs',
getScene: getTab1Scene,
}),
new SceneAppPage({
title: 'Tab 1',
url: '/my-page/tabs/tab-two',
getScene: getTab2Scene,
}),
],
}),
],
});
可以用來建立獨立的 Dashboard 或視覺化頁面,將複雜的視覺化嵌入其他 Grafana 頁面或外部應用,或作為更大型應用結構(如 SceneApp)中的一個頁面或圖表。EmbeddedScene 包括內建自己的資料源、時間範圍和變數以及以下兩個屬性:
const getScene = () => new EmbeddedScene({
body: new SceneFlexLayout({
children: [
new SceneFlexItem({
body: new VizPanel({...}),
}),
],
}),
controls: [
new VariableValueSelectors({}),
new SceneTimePicker({}),
],
});
建立的最終步驟,就是將定義的 getScene 調用,返回一個 SceneObject 實例,並利用這個實例中的 Component 的屬性渲染出一個應用程式:
export const DemoPluginPage = () => {
const scene = getScene();
return <scene.Component model={scene} />;
};
筆者語錄
就如同在一段落所比喻,Grafana Scenes 就像一個俄羅斯娃娃,每一層都是 Scene Object,從SceneApp
到EmbeddedScene
,再到最小的SceneFlexItem
,都能相互嵌套。搞懂它的State
、Model
和Render
後,其實也沒有那麼複雜,尤其是有了這麼多現成的元件,只要好好搭配,就能輕鬆構建一個 Grafana 的應用程式。而下一章節中,我們將認識 Grafana Scenes 又提供了什麼元件,讓開發者可以更輕鬆的打造一個 Scene App 的 Layout,並在之後的文章一篇一腳印的從資料取得、模板化到 Panel 呈現及進階設定,深度地剖析 Grafana Scenes。