Drill-down pages in Scenes apps
Drill-down pages are a powerful tool for building complex, data-driven applications. They allow you to create a high-level overview of the data that a user can interact with and progressively explore to reveal underlying data.
Add drill-down pages to Scenes apps
SceneAppPage
comes with an API that allows you to create deep, nested drill-down pages.
Before you begin: You must already know about React Router URL params, Grafana field configuration, and data links before continuing with this guide.
To create a drill-down page, use the drilldown
property of the SceneAppPage
object.
Step 1. Create a Scenes app
Follow the Building apps with Scenes guide to build your app.
Step 2. Build a top level drill-down page
Use the code that follows to build a page that shows a summary of the average duration of HTTP requests for Prometheus API endpoints, using Grafana's Table panel:
function getOverviewScene() {
const queryRunner = new SceneQueryRunner({
$timeRange: new SceneTimeRange(),
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: 'sort_desc(avg by(handler) (rate(prometheus_http_request_duration_seconds_sum {}[5m]) * 1e3))',
format: 'table',
instant: true,
},
],
});
const tablePanel = PanelBuilders.table().setTitle('Average duration of HTTP request').setData(queryRunner).build();
return new EmbeddedScene({
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
minHeight: 300,
body: tablePanel,
}),
],
}),
});
}
function getSceneApp() {
return new SceneApp({
pages: [
new SceneAppPage({
title: 'HTTP handlers overview',
url: '/a/<PLUGIN_ID>/my-app',
getScene: getOverviewScene,
}),
],
});
}
Step 3. Set up drill-down navigation
To show the drill-down page, you need to provide navigation. Configure Table panel data links (learn about data links in the official Grafana documentation). Then modify the Table panel configuration to set up a data link for the handler
field:
import { sceneUtils, PanelBuilders } from '@grafana/scenes';
// ...
const tablePanel = PanelBuilders.table()
.setTitle('Average duration of HTTP request')
.setData(queryRunner)
.setOverrides((b) =>
b.matchFieldsWithName('handler').overrideLinks([
{
title: 'Go to handler overview',
url: '/a/<PLUGIN_ID>/my-app/${__value.text}${__url.params}',
},
])
)
.build();
The resulting panel will have links for all values of the handler
field. Clicking a value will redirect to a particular endpoint drill-down URL that will show a "Not found page" error. You'll set up this page in the next step.
The fieldConfig
options are the same options you would see in typical dashboard panels when you view Panel JSON from the Table panel inspect drawer. To access panel inspect drawer, click Inspect in the panel edit menu.
Step 4. Build a drill-down page
Modify the getSceneApp
function to set up drill-down scenes. Use the drilldowns
property of the SceneAppPage
object. The drilldowns
property accepts an array of SceneAppDrilldownView
objects. It allows a drill-down URL and page to be rendered configuration:
export interface SceneAppDrilldownView {
/** Use to provide parametrized drilldown URL, for example, /app/clusters/:clusterId **/
routePath: string;
/** Function that returns a page object for a given drilldown route match. Use parent to configure drilldown view parent SceneAppPage via getParentPage method. **/
getPage: (routeMatch: SceneRouteMatch<any>, parent: SceneAppPageLike) => SceneAppPageLike;
}
Configure the API endpoint drill-down view:
function getSceneApp() {
return new SceneApp({
pages: [
new SceneAppPage({
title: 'HTTP handlers overview',
url: '/a/<PLUGIN_ID>/my-app',
getScene: getOverviewScene,
drilldowns: [
{
routePath: '/a/<PLUGIN_ID>/my-app/:handler',
getPage: getHandlerDrilldownPage,
},
],
}),
],
});
}
Define a function that returns a SceneAppPage
for a drill-down view. This function receives two arguments:
routeMatch
- Contains information about URL params.parentPage
- Contains a reference to the parentSceneAppPage
required to configure breadcrumbs correctly.
function getHandlerDrilldownPage(routeMatch: SceneRouteMatch<{ handler: string }>, parent: SceneAppPageLike) {
// Retrieve handler from the URL params
const handler = decodeURIComponent(routeMatch.params.handler);
return new SceneAppPage({
// Set up a particular handler drill-down URL
url: `/a/<PLUGIN_ID>/my-app/${encodeURIComponent(handler)}`,
// Important: Set this up for breadcrumbs to be built
getParentPage: () => parent,
title: `${handler} endpoint overview`,
getScene: () => getHandlerDrilldownScene(handler),
});
}
Step 5. Build a drill-down scene
Define a scene that will be rendered on the drill-down page:
function getHandlerDrilldownScene(handler: string) {
const requestsDuration = new SceneQueryRunner({
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: `avg without(job, instance) (rate(prometheus_http_request_duration_seconds_sum{handler="${handler}"}[5m])) * 1e3`,
},
],
});
const requestsCount = new SceneQueryRunner({
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: `sum without(job, instance) (rate(prometheus_http_request_duration_seconds_count{handler="${handler}"}[5m])) `,
},
],
});
return new EmbeddedScene({
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries().setTitle('Requests duration').setData(requestsDuration),
}),
new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries().setTitle('Requests count').setData(requestsCount),
}),
],
}),
});
}
Complete example
Below you'll find the complete code for a Scenes app with drill-down pages:
function getOverviewScene() {
const queryRunner = new SceneQueryRunner({
$timeRange: new SceneTimeRange(),
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: 'sort_desc(avg by(handler) (rate(prometheus_http_request_duration_seconds_sum {}[5m]) * 1e3))',
format: 'table',
instant: true,
},
],
});
const tablePanel = PanelBuilders.table()
.setTitle('Average duration of HTTP request')
.setData(queryRunner)
.setOverrides((b) =>
b.matchFieldsWithName('handler').overrideLinks([
{
title: 'Go to handler overview',
url: '/a/<PLUGIN_ID>/my-app/${__value.text}${__url.params}',
},
])
)
.build();
return new EmbeddedScene({
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
minHeight: 300,
body: tablePanel,
}),
],
}),
});
}
function getHandlerDrilldownPage(routeMatch: SceneRouteMatch<{ handler: string }>, parent: SceneAppPageLike) {
// Retrieve handler from the URL params.
const handler = decodeURIComponent(routeMatch.params.handler);
return new SceneAppPage({
// Setup particular handler drilldown URL
url: `/a/<PLUGIN_ID>/my-app/${encodeURIComponent(handler)}`,
// Important: setup this for breadcrumbs to be built
getParentPage: () => parent,
title: `${handler} endpoint overview`,
getScene: () => getHandlerDrilldownScene(handler),
});
}
function getHandlerDrilldownScene(handler: string) {
const requestsDuration = new SceneQueryRunner({
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: `avg without(job, instance) (rate(prometheus_http_request_duration_seconds_sum{handler="${handler}"}[5m])) * 1e3`,
},
],
});
const requestsCount = new SceneQueryRunner({
datasource: {
type: 'prometheus',
uid: '<PROVIDE_GRAFANA_DS_UID>',
},
queries: [
{
refId: 'A',
expr: `sum without(job, instance) (rate(prometheus_http_request_duration_seconds_count{handler="${handler}"}[5m])) `,
},
],
});
return new EmbeddedScene({
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries().setTitle('Requests duration').setData(requestsDuration),
}),
new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries().setTitle('Requests count').setData(requestsCount),
}),
],
}),
});
}
function getSceneApp() {
return new SceneApp({
pages: [
new SceneAppPage({
title: 'HTTP handlers overview',
url: '/a/<PLUGIN_ID>/my-app',
getScene: getOverviewScene,
drilldowns: [
{
routePath: '/a/<PLUGIN_ID>/my-app/:handler',
getPage: getHandlerDrilldownPage,
},
],
}),
],
});
}