為了讓開發者可以處理更複雜的視覺化呈現,本篇文章將介紹 Grafana 的一些進階功能。首先,我們會介紹 addActivationHandler
函數,一個能夠輕鬆地為核心 Scenes 元件添加外部行為。接著,我們會討論如何使用 Behaviors
來實現自定義 Scenes 邏輯,這些邏輯可以在 Scenes Object 啟動時作為初始化執行。此外,我們還會介紹 URL sync
功能,這是一種能夠讓 Scenes Object 的狀態與 URL 同步的強大工具,並深入了解如何利用 SceneByFrameRepeater
進行多重的 Scenes 圖表更新。這些進階功能不僅可以提升 Grafana 的可用性,也能滿足更複雜的視覺化需求。
addActivationHandler
為 SceneObject
中的一個函數,讓使用更自由地為 Scenes 元件添加外部行為。addActivationHandler
會在核心的 Scenes 元件或 SceneObject
被啟動執行,並返回一個停用(deactivation)的處理函數,在 SceneObject
卸載時會調用並將 addActivationHandler
中的行為清除。而一個 SceneObject
啟動的時機可能在於初渲染、因數據資料改變或使用者行為觸發,因此這個機制與 React 中的 useEffect
非常相似。
在 Scenes 的元件中也大量地使用這個函數,以 EmbeddedScene 為例,調用 addActivationHandler
做初始化處理並做清理的機制:
public constructor(state: EmbeddedSceneState) {
super(state);
this.addActivationHandler(() => {
const unsetGlobalScene = setWindowGrafanaSceneContext(this);
return () => {
unsetGlobalScene();
};
});
}
💡NOTICE
在此函數功能新增之前,若要實現相同功能需要覆蓋activate
然後調用super.activate()
的自定義元件,實作起來複雜很多。相關更新可以參考 https://github.com/grafana/scenes/pull/77。
SceneObject
被啟用時執行特定的程式碼。適用於初始化、資料載入、設定訂閱等操作。addActivationHandler
會返回一個清理函數,這個函數會在 Scenes 停用時自動執行。確保資源能被正確釋放,避免潛在的記憶體洩漏。以下範例為 SceneQueryRunner 的 addActivationHandler
會在 customObject 狀態變化時更新 queryRunner 的狀態,進而依據更新的狀態進行請求。其中會使用 subscribeToState 去監聽 customObject 的狀態,並在最後清除這個監聽的行為。
Grafana Scene 提供
subscribeToState
的函數為SceneObject
監聽狀態。
const queryRunner = new SceneQueryRunner({...})
queryRunner.addActivationHandler(() => {
// 開始監聽
const sub = customObject.subscribeToState((newState) => {
queryRunner.setState({
queries: [
{ ...queryRunner.state.queries[0], seriesCount: newState.counter },
],
});
queryRunner.runQueries();
});
// 清除監聽的行為
return () => {
sub.unsubscribe();
};
});
Behaviors 可以用來實現自定義的 Scenes 邏輯,這些邏輯作為 effect 執行。就像 utils 或 hooks 一樣,你可以在這個 SceneObject
中定義任何自定義的邏輯,例如有條件地隱藏 Scenes 中的元素,或者在不同的 SceneObject
之間添加共享功能。
每一個 SceneObject
都有一個 $behaviors
屬性,可以包含多個 Behaviors 實例或函數。當父層 SceneObject
被活化時,這些 Behaviors 也會同步活化,因此可以同時取得自身以及父層的狀態。Behaviors 可以通過兩種方式來實現:
(一)純函數方式實現 Behaviors
將 Behavior 定義為一個無狀態的函數,當其父層 Object 被活化時自動調用。純函數可以返回一個清理函數,用於在父層 Object 被卸載或停用時進行清理或執行收尾操作,例如釋放資源或移除監聽器。這種無狀態特性適合用於日誌記錄、簡單事件處理或在特定生命週期點執行的一次性操作。
(二)SceneObject方式實現 Behaviors
建立一個完整的 SceneObject
,擁有自己的生命週期和狀態管理,適合需要維護內部狀態或處理複雜邏輯的情況。不僅可以在活化和去活化時執行操作,還可以響應其他 Scene 事件,並根據需要更新自身狀態。例如監視狀態變化、影響父層 Object 的行為或修改其狀態。
上述的兩種實現方法是屬於開發者自定義的模式,不過為了方便操作,Grafana Scenes 也提供了幾個已經封裝好的 API,為因應一些常用的情境,包括 ActWhenVariableChanged、CursorSync、SceneQueryController 以及 LiveNowTimer,以下為各自的使用介紹:
這個 Behavior 用於監聽 Variable 的變化並觸發相應的動作。當 SceneObject
中的 Variable 發生變化時,執行特定的操作,如重新請求數據資料或更新圖表。
此 API 有兩個屬性:指定的變數名稱 variableName 以及當變數改變時所執行的操作 onChange,其中可以在 onChange 取得兩個參數:variable 以及 behavior
sceneGraph
以當前的 behavior 位置查找其他的 SceneObject,例如同層的 $data、$variables、$timeRange。原理
當 Scene 被活化後,這個 behavior 開始監聽指定變數的變化。每當變數值發生改變,就會觸發預設的回調函數。這個回調函數可能會返回一個取消函數,用於清理可能的異步操作。在變數再次改變之前,如果存在之前的取消函數,系統會先執行它,以確保舊的操作被適當地終止。這種機制適用於網路請求或複雜的計算操作。可以確保只有最新的操作會被完整執行,有效地管理資源並維護系統的響應性。
範例
$behaviors: [
new behaviors.ActWhenVariableChanged({
variableName: 'subTab',
onChange: (variable) => {
const value = variable.getValue();
const subTab = subTabs.find(({ option }) => option === value);
subTabBody.setState({ body: subTab?.getPanels() });
},
}),
],
建立一個共享的游標範圍,用於同步多個圖表的游標位置。例如在 Dashboard 上有多個 time series 圖表時,使用者在一個圖表上移動游標,其他圖表也會同步顯示相同時間點的數據。此 behavior 有一個 sync 的屬性,而 sync 的模式有三種:Crosshair、Off 及 Tooltip:
Crosshair:在一個圖表上移動游標時,會在有同步的游標位置顯示一個垂直和水平的交叉線條。
Tooltip:除了 Crosshair 的行為,也會同時展開顯示 tooltip。
Off:為預設狀態,不顯示 Tooltip 也不顯示 Crosshair。
特點
DashboardCursorSync.Off
。範例
由於 CursorSync 有父層依賴的特性,所以通常會將 new behaviors.CursorSync
設置於 SceneFlexLayout 的層級:
new SceneFlexLayout({
direction: 'column',
$behaviors: [new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair })],
children: [
new SceneFlexItem({
minHeight: MAIN_PANEL_MIN_HEIGHT,
maxHeight: MAIN_PANEL_MAX_HEIGHT,
body: new AutoVizPanel({}),
}),
new SceneFlexItem({
ySizing: 'content',
body: new MetricActionBar({}),
}),
],
});
這個 Behavior 功能提供了一個中央機制來追蹤和管理所有正在進行的 Query。
原理
當 Query 被註冊時,Controller 會包裝原始的 Observable 物件,添加額外的邏輯來追蹤 Query 的運行狀態。此外還提供了取消所有 Query 的功能(cancelAll),可使用在需要快速重置 Scenes 狀態的情況。而 SceneQueryController 處理異步操作、 Query 的執行和清理,皆是使用 RxJS 的 Pipe 和訂閱機制。
範例
通常在 Scenes 中使用這個 behavior 時,會是 Query 有 lazy 的請求狀態,且 Refresh picker 的元件會有 Cancel 顯示,點擊後使用者可以取消 Query 行為:
https://github.com/grafana/scenes/pull/513
$behaviors: [new behaviors.SceneQueryController()]
LiveNowTimer 提供一個即時更新的計時器。在 Dashboard 中顯示當前時間,或者定期觸發數據 refresh。
原理
enable()
和 disable()
方法來啟用或禁用計時器。範例
在 Scenes 中使用時建議使用最小的 Refresh 頻率最為自動更新的速率。最後顯示的效果會像跑馬燈一樣取得當前的 timeRange 動態更新 Panel 到 ‘now’ 的時間點。
https://github.com/grafana/grafana/pull/37769
const timer = new behaviors.LiveNowTimer({ enabled: true });
const scene = new EmbeddedScene({
$behaviors: [timer],
body: new SceneFlexLayout({
children: [
new SceneFlexItem({
body: new VizPanel({}),
}),
new SceneFlexItem({
body: new VizPanel({}),
}),
],
}),
});
const timerHandler = () => timer.disable()
getUrlSyncManager
是藉由 UrlSyncManager
建立的 uitls,UrlSyncManager
是通過監聽SceneObject 的狀態變化和 URL 的變化來實現雙向同步。當 SceneObject 的狀態發生變化時,UrlSyncManager 會更新 URL 參數;反之,當 URL 發生變化時,它會相應地更新 SceneObject 的狀態。也提供了一系列方法如 initSync、cleanUp、handleNewLocation 和 handleNewObject 來管理同步過程的生命週期。UrlSyncManager 在處理 URL 參數被移除的情況時可以更加靈活,開發者可以在 updateFromUrl
方法中自定義行為。
initSync
目的是初始化 URL 同步,會訂閱 Root SceneObject 的狀態變化事件,需要在 Scene 渲染之前調用,以確保 SceneObject 在被活化之前處於同步狀態。這可以避免一些不必要的 re-render,並確保按需發出變數查詢。
useEffect(() => {
if (!isInitialized) {
getUrlSyncManager().initSync(scene);
setIsInitialized(true);
}
}, [scene, isInitialized]);
cleanUp
清理 URL 同步相關的資源。例如:取消訂閱狀態變化事件或重設內部狀態。
public _onActivate() {
return () => {
getUrlSyncManager().cleanUp(this);
};
}
handleNewLocation
當 URL 發生變化時,將 URL 狀態同步 SceneObject。使用時會以 @grafana/runtime
的 locationService 取得最新的位置,在作為其參數:
useEffect(() => {
const latestLocation = locationService.getLocation();
const locationToHandle = latestLocation !== location ? latestLocation : location;
urlSyncManager.handleNewLocation(locationToHandle);
}, [sceneRoot, urlSyncManager, location]);
handleNewObject
將 URL 狀態同步到新添加的 SceneObject。
const urlSyncManager = getUrlSyncManager();
urlSyncManager.initSync(sceneRoot);
urlSyncManager.handleNewLocation(location);
urlSyncManager.handleNewObject(newSceneObject);
https://github.com/grafana/scenes/pull/618
https://community.grafana.com/t/refresh-live-dashboards/84959