grafana-scenes

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

@grafana/scenes Framework

@grafana/scenes框架

Build reactive, data-driven Grafana plugin pages with declarative scene objects.
使用声明式场景对象构建响应式、数据驱动的Grafana插件页面。

Core Concepts

核心概念

Scenes composes a tree of objects:
SceneApp
SceneAppPage
EmbeddedScene
→ layouts → panels. Each node can own data (
$data
), variables (
$variables
), time ranges (
$timeRange
), and behaviors (
$behaviors
) that propagate down the tree.
场景由对象树组成:
SceneApp
SceneAppPage
EmbeddedScene
→ 布局 → 面板。每个节点都可以拥有数据(
$data
)、变量(
$variables
)、时间范围(
$timeRange
)和行为(
$behaviors
),这些会向下传递到整个对象树中。

Quick Start: New Scene Page

快速入门:新建场景页面

1. Create the scene file

1. 创建场景文件

typescript
// src/components/scenes/MyFeature/scene.tsx
import {
  EmbeddedScene, SceneFlexLayout, SceneFlexItem,
  SceneQueryRunner, SceneVariableSet, QueryVariable,
  PanelBuilders, VariableValueSelectors, SceneControlsSpacer,
} from '@grafana/scenes';

export function getMyFeatureScene(params: { datasource: DataSourceRef }) {
  const queryRunner = new SceneQueryRunner({
    datasource: params.datasource,
    queries: [{ refId: 'A', expr: 'up{cluster=~"$cluster"}', instant: true, format: 'table' }],
  });

  const panel = PanelBuilders.table()
    .setData(queryRunner)
    .setTitle('My Table')
    .build();

  return new EmbeddedScene({
    $variables: new SceneVariableSet({
      variables: [
        new QueryVariable({
          name: 'cluster',
          query: 'label_values(up, cluster)',
          datasource: params.datasource,
          isMulti: true, includeAll: true, defaultToAll: true,
        }),
      ],
    }),
    controls: [new VariableValueSelectors({}), new SceneControlsSpacer()],
    body: new SceneFlexLayout({
      direction: 'column',
      children: [new SceneFlexItem({ body: panel })],
    }),
  });
}
typescript
// src/components/scenes/MyFeature/scene.tsx
import {
  EmbeddedScene, SceneFlexLayout, SceneFlexItem,
  SceneQueryRunner, SceneVariableSet, QueryVariable,
  PanelBuilders, VariableValueSelectors, SceneControlsSpacer,
} from '@grafana/scenes';

export function getMyFeatureScene(params: { datasource: DataSourceRef }) {
  const queryRunner = new SceneQueryRunner({
    datasource: params.datasource,
    queries: [{ refId: 'A', expr: 'up{cluster=~"$cluster"}', instant: true, format: 'table' }],
  });

  const panel = PanelBuilders.table()
    .setData(queryRunner)
    .setTitle('My Table')
    .build();

  return new EmbeddedScene({
    $variables: new SceneVariableSet({
      variables: [
        new QueryVariable({
          name: 'cluster',
          query: 'label_values(up, cluster)',
          datasource: params.datasource,
          isMulti: true, includeAll: true, defaultToAll: true,
        }),
      ],
    }),
    controls: [new VariableValueSelectors({}), new SceneControlsSpacer()],
    body: new SceneFlexLayout({
      direction: 'column',
      children: [new SceneFlexItem({ body: panel })],
    }),
  });
}

2. Create the page

2. 创建页面

typescript
// src/components/scenes/MyFeature/MyFeature.tsx
import { SceneAppPage, SceneTimeRange } from '@grafana/scenes';

export function getMyFeaturePage(params) {
  return new SceneAppPage({
    title: 'My Feature',
    url: '/a/my-plugin-id/my-feature',
    routePath: 'my-feature/*',
    $timeRange: new SceneTimeRange({ from: 'now-1h', to: 'now' }),
    getScene: () => getMyFeatureScene(params),
    drilldowns: [],
  });
}
typescript
// src/components/scenes/MyFeature/MyFeature.tsx
import { SceneAppPage, SceneTimeRange } from '@grafana/scenes';

export function getMyFeaturePage(params) {
  return new SceneAppPage({
    title: 'My Feature',
    url: '/a/my-plugin-id/my-feature',
    routePath: 'my-feature/*',
    $timeRange: new SceneTimeRange({ from: 'now-1h', to: 'now' }),
    getScene: () => getMyFeatureScene(params),
    drilldowns: [],
  });
}

3. Register in SceneApp

3. 在SceneApp中注册

Add the page to the
SceneApp
pages array in the root scene file.
将页面添加到根场景文件中的
SceneApp
页面数组中。

Key Patterns

关键模式

Drilldowns (click-through navigation)

钻取(点击跳转导航)

typescript
drilldowns: [{
  routePath: ':cluster/*',
  getPage: (match, parent) => new SceneAppPage({
    title: decodeURIComponent(match.params.cluster),
    url: `${parent.state.url}/${match.params.cluster}`,
    routePath: `${match.params.cluster}/*`,
    getScene: () => detailScene(decodeURIComponent(match.params.cluster)),
  }),
}]
typescript
drilldowns: [{
  routePath: ':cluster/*',
  getPage: (match, parent) => new SceneAppPage({
    title: decodeURIComponent(match.params.cluster),
    url: `${parent.state.url}/${match.params.cluster}`,
    routePath: `${match.params.cluster}/*`,
    getScene: () => detailScene(decodeURIComponent(match.params.cluster)),
  }),
}]

Tabs (sub-pages within a detail view)

标签页(详情视图内的子页面)

Pass
tabs: [SceneAppPage, ...]
instead of
getScene
on a
SceneAppPage
. Each tab is itself a
SceneAppPage
with its own scene.
SceneAppPage
上传递
tabs: [SceneAppPage, ...]
替代
getScene
。每个标签页本身就是一个拥有独立场景的
SceneAppPage

Query with transformations

带转换的查询

Wrap a
SceneQueryRunner
in
SceneDataTransformer
to apply Grafana transforms or custom RxJS operators:
typescript
new SceneDataTransformer({
  $data: queryRunner,
  transformations: [
    { id: 'organize', options: { renameByName: { 'Value #A': 'CPU' } } },
    (ctx) => (source) => source.pipe(map((frames) => /* custom transform */)),
  ],
})
SceneQueryRunner
包裹在
SceneDataTransformer
中,以应用Grafana转换或自定义RxJS操作符:
typescript
new SceneDataTransformer({
  $data: queryRunner,
  transformations: [
    { id: 'organize', options: { renameByName: { 'Value #A': 'CPU' } } },
    (ctx) => (source) => source.pipe(map((frames) => /* custom transform */)),
  ],
})

Custom scene object

自定义场景对象

Extend
SceneObjectBase
with a static
Component
for custom interactive UI:
typescript
class MyWidget extends SceneObjectBase<MyWidgetState> {
  static Component = ({ model }: SceneComponentProps<MyWidget>) => {
    const state = model.useState();
    return <div>{state.value}</div>;
  };
}
继承
SceneObjectBase
并添加静态
Component
以实现自定义交互式UI:
typescript
class MyWidget extends SceneObjectBase<MyWidgetState> {
  static Component = ({ model }: SceneComponentProps<MyWidget>) => {
    const state = model.useState();
    return <div>{state.value}</div>;
  };
}

Table column overrides

表格列覆盖

Build
ConfigOverrideRule
objects for drill-down links, filtering, units, widths, custom cells.
创建
ConfigOverrideRule
对象以实现钻取链接、过滤、单位设置、宽度调整、自定义单元格等功能。

Panel types

面板类型

PanelBuilders.table()
,
.timeseries()
,
.stat()
,
.gauge()
,
.barchart()
— chain
.setData()
,
.setTitle()
,
.setUnit()
,
.setOption()
,
.setOverrides()
, then
.build()
.
PanelBuilders.table()
.timeseries()
.stat()
.gauge()
.barchart()
—— 通过链式调用
.setData()
.setTitle()
.setUnit()
.setOption()
.setOverrides()
,然后调用
.build()
完成构建。

Common Pitfalls

常见陷阱

  • Always use
    routePath: 'path/*'
    (with wildcard) on pages that have drilldowns or tabs
  • encodeURIComponent
    /
    decodeURIComponent
    URL params — K8s names can contain
    /
  • Variables referenced in queries as
    $varName
    must exist in an ancestor
    SceneVariableSet
  • getScene
    is called lazily; don't create side effects in the factory
  • For instant queries, set both
    instant: true
    and
    format: 'table'
  • 对于包含钻取或标签页的页面,务必使用
    routePath: 'path/*'
    (带通配符)
  • 对URL参数使用
    encodeURIComponent
    /
    decodeURIComponent
    —— K8s名称可能包含
    /
  • 查询中引用的变量
    $varName
    必须存在于祖先
    SceneVariableSet
  • getScene
    是延迟调用的;不要在工厂函数中产生副作用
  • 对于即时查询,需同时设置
    instant: true
    format: 'table'

Resources

资源