From grafana-app-sdk
Build Grafana plugin pages using @grafana/scenes framework: create scene pages, add panels and visualizations, configure drilldowns, variables, query runners, and layouts.
npx claudepluginhub grafana/skills --plugin grafana-app-sdkThis skill uses the workspace's default tool permissions.
Build reactive, data-driven Grafana plugin pages with declarative scene objects.
Creates, modifies, and organizes Grafana dashboards including panels (time series, stat, table, gauge), variables, transformations, thresholds, and alerting.
Builds production-ready Grafana dashboards with panels, template variables, annotations, and provisioning for Prometheus/Loki metrics. Use for SRE operational views, SLO reporting, or version-controlled deployments.
Creates Grafana dashboards with panels, variables, templating, alerts, and provisioning. Use for monitoring dashboards, time-series visualizations, and operational insights.
Share bugs, ideas, or general feedback.
Build reactive, data-driven Grafana plugin pages with declarative scene objects.
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.
// 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 })],
}),
});
}
// 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: [],
});
}
Add the page to the SceneApp pages array in the root scene file.
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)),
}),
}]
Pass tabs: [SceneAppPage, ...] instead of getScene on a SceneAppPage. Each tab is itself a SceneAppPage with its own scene.
Wrap a SceneQueryRunner in SceneDataTransformer to apply Grafana transforms or custom RxJS operators:
new SceneDataTransformer({
$data: queryRunner,
transformations: [
{ id: 'organize', options: { renameByName: { 'Value #A': 'CPU' } } },
(ctx) => (source) => source.pipe(map((frames) => /* custom transform */)),
],
})
Extend SceneObjectBase with a static Component for custom interactive UI:
class MyWidget extends SceneObjectBase<MyWidgetState> {
static Component = ({ model }: SceneComponentProps<MyWidget>) => {
const state = model.useState();
return <div>{state.value}</div>;
};
}
Build ConfigOverrideRule objects for drill-down links, filtering, units, widths, custom cells.
PanelBuilders.table(), .timeseries(), .stat(), .gauge(), .barchart() — chain .setData(), .setTitle(), .setUnit(), .setOption(), .setOverrides(), then .build().
routePath: 'path/*' (with wildcard) on pages that have drilldowns or tabsencodeURIComponent/decodeURIComponent URL params — K8s names can contain /$varName must exist in an ancestor SceneVariableSetgetScene is called lazily; don't create side effects in the factoryinstant: true and format: 'table'