Documentation Index
Fetch the complete documentation index at: https://mintlify.com/grafana/grafana/llms.txt
Use this file to discover all available pages before exploring further.
App Plugin Development
App plugins allow you to create complete applications within Grafana, including custom pages, navigation, and bundled data sources or panels. This guide covers building app plugins.
Overview
App plugins can include:
- Custom pages: Full-page React applications
- Navigation items: Custom menu entries
- Bundled plugins: Include data source or panel plugins
- Dashboards: Pre-built dashboards
- Backend components: Server-side logic and APIs
- Plugin extensions: Extend other plugins’ functionality
Creating an App Plugin
Plugin Entry Point
App plugins extend the AppPlugin class from @grafana/data:
// module.tsx
import { AppPlugin } from '@grafana/data';
import { AppConfig } from './components/AppConfig';
import { RootPage } from './pages/RootPage';
export const plugin = new AppPlugin<{}>() // No settings needed
.setRootPage(RootPage)
.addConfigPage({
title: 'Configuration',
icon: 'cog',
body: AppConfig,
id: 'configuration',
});
Root Page Component
The root page handles routing and navigation:
import React from 'react';
import { AppRootProps } from '@grafana/data';
import { Route, Routes } from 'react-router-dom';
import { HomePage } from './HomePage';
import { DetailsPage } from './DetailsPage';
export const RootPage: React.FC<AppRootProps> = ({ path, meta }) => {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/details/:id" element={<DetailsPage />} />
</Routes>
);
};
Configuration Page
Provide settings for your app:
import React from 'react';
import { AppPluginMeta, PluginConfigPageProps } from '@grafana/data';
import { Button, Field, Input } from '@grafana/ui';
interface AppSettings {
apiUrl?: string;
apiKey?: string;
}
export const AppConfig: React.FC<PluginConfigPageProps<AppPluginMeta<AppSettings>>> = ({
plugin,
}) => {
const { enabled, jsonData, secureJsonFields } = plugin.meta;
const [settings, setSettings] = React.useState<AppSettings>({
apiUrl: jsonData?.apiUrl || '',
});
const onSave = async () => {
const response = await fetch(`/api/plugins/${plugin.meta.id}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled,
jsonData: settings,
secureJsonData: { /* secure fields */ },
}),
});
};
return (
<div>
<Field label="API URL">
<Input
value={settings.apiUrl}
onChange={(e) => setSettings({ ...settings, apiUrl: e.currentTarget.value })}
/>
</Field>
<Button onClick={onSave}>Save</Button>
</div>
);
};
Navigation Configuration
Define navigation in plugin.json:
{
"type": "app",
"name": "My Application",
"id": "myorg-myapp-app",
"includes": [
{
"type": "page",
"name": "Home",
"path": "/a/myorg-myapp-app",
"role": "Viewer",
"addToNav": true,
"defaultNav": true,
"icon": "home"
},
{
"type": "page",
"name": "Settings",
"path": "/a/myorg-myapp-app/settings",
"role": "Admin",
"addToNav": true,
"icon": "cog"
},
{
"type": "dashboard",
"name": "Overview Dashboard",
"path": "dashboards/overview.json",
"addToNav": true,
"uid": "myapp-overview"
}
]
}
Include Types
page: Custom page route
dashboard: Bundled dashboard
datasource: Bundled data source plugin
panel: Bundled panel plugin
Include Properties
name: Display name in navigation
path: URL path or file path (for dashboards)
role: Minimum required role (Viewer, Editor, Admin)
action: RBAC action required (overrides role)
addToNav: Add to navigation menu
defaultNav: Make this the default page
icon: Icon name from @grafana/ui
uid: Dashboard UID (for dashboard includes)
Backend Implementation
App plugins can include backend components for custom APIs:
// pkg/plugin/app.go
package plugin
import (
"context"
"encoding/json"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
type App struct {
backend.CallResourceHandler
}
func NewApp() *App {
return &App{}
}
func (a *App) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
switch req.Path {
case "health":
return a.handleHealth(ctx, req, sender)
case "data":
return a.handleData(ctx, req, sender)
default:
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusNotFound,
})
}
}
func (a *App) handleHealth(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
body, _ := json.Marshal(map[string]string{"status": "ok"})
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Body: body,
})
}
Frontend API Calls
Call backend resources from the frontend:
import { getBackendSrv } from '@grafana/runtime';
export const fetchData = async () => {
const response = await getBackendSrv().fetch({
url: '/api/plugins/myorg-myapp-app/resources/data',
method: 'GET',
});
return response.data;
};
Plugin Extensions
Extend other plugins’ functionality:
Registering Extensions
In plugin.json:
{
"extensions": {
"addedLinks": [
{
"targets": ["grafana/dashboard/panel/menu"],
"title": "Open in My App",
"description": "View this panel in My Application"
}
],
"addedComponents": [
{
"targets": ["grafana/explore/toolbar"],
"title": "Custom Toolbar Button",
"description": "Adds a custom button to Explore toolbar"
}
],
"exposedComponents": [
{
"id": "myorg/custom-widget",
"title": "Custom Widget",
"description": "Reusable widget for other plugins"
}
],
"extensionPoints": [
{
"id": "myorg/myapp/action",
"title": "My App Actions",
"description": "Extension point for custom actions"
}
]
}
}
Implementing Extension Components
import { usePluginLinks, usePluginComponents } from '@grafana/runtime';
export const MyComponent = () => {
// Get extension links
const { links } = usePluginLinks({
extensionPointId: 'grafana/dashboard/panel/menu',
});
// Get extension components
const { components } = usePluginComponents({
extensionPointId: 'myorg/myapp/action',
});
return (
<div>
{components.map((Component, i) => (
<Component key={i} />
))}
</div>
);
};
Role-Based Access Control
Define custom roles and permissions:
{
"roles": [
{
"role": {
"name": "My App Viewer",
"description": "Can view data in My App",
"permissions": [
{
"action": "myorg.myapp:read",
"scope": "myorg.myapp:*"
}
]
},
"grants": ["Viewer"]
},
{
"role": {
"name": "My App Editor",
"description": "Can edit data in My App",
"permissions": [
{
"action": "myorg.myapp:write",
"scope": "myorg.myapp:*"
}
]
},
"grants": ["Editor"]
}
]
}
Bundling Other Plugins
Include data source or panel plugins:
{
"includes": [
{
"type": "datasource",
"name": "My App Data Source"
},
{
"type": "panel",
"name": "My App Panel"
}
],
"dependencies": {
"grafanaVersion": ">=9.0.0",
"plugins": []
}
}
Plugin.json Configuration
Complete app plugin configuration:
{
"type": "app",
"name": "My Application",
"id": "myorg-myapp-app",
"info": {
"description": "Custom application for Grafana",
"author": {
"name": "My Organization",
"url": "https://example.com"
},
"keywords": ["app", "custom"],
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"links": [
{
"name": "Documentation",
"url": "https://example.com/docs"
}
],
"version": "1.0.0",
"updated": "2024-01-01"
},
"includes": [],
"dependencies": {
"grafanaVersion": ">=9.0.0",
"plugins": []
},
"backend": true,
"executable": "gpx_myorg_myapp",
"autoEnabled": false
}
Key Configuration Options
autoEnabled: Auto-enable plugin on installation
backend: Has backend component
executable: Backend binary name
includes: Bundled pages, dashboards, and plugins
roles: RBAC role definitions
extensions: Plugin extensions configuration
State Management
Use React hooks and context for state:
import React, { createContext, useContext, useState } from 'react';
interface AppState {
data: any[];
loading: boolean;
}
const AppContext = createContext<AppState | undefined>(undefined);
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<AppState>({
data: [],
loading: false,
});
return <AppContext.Provider value={state}>{children}</AppContext.Provider>;
};
export const useAppState = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context;
};
Routing
Use React Router for navigation:
import { useLocation, useNavigate } from 'react-router-dom';
export const MyPage = () => {
const location = useLocation();
const navigate = useNavigate();
const goToDetails = (id: string) => {
navigate(`/a/myorg-myapp-app/details/${id}`);
};
return <button onClick={() => goToDetails('123')}>View Details</button>;
};
Best Practices
- Follow Grafana conventions: Use consistent URL paths (
/a/plugin-id/page)
- Implement proper RBAC: Use roles and actions for access control
- Handle plugin lifecycle: Implement enable/disable functionality
- Provide configuration UI: Make setup easy for users
- Bundle useful dashboards: Include pre-built visualizations
- Document extensions: Clearly document extension points
- Test thoroughly: Test all pages and navigation paths
- Support theming: Use
@grafana/ui components for theme compatibility
Common Patterns
Loading States
import { LoadingPlaceholder } from '@grafana/ui';
export const MyPage = () => {
const [loading, setLoading] = useState(true);
if (loading) {
return <LoadingPlaceholder text="Loading..." />;
}
return <div>Content</div>;
};
Error Handling
import { Alert } from '@grafana/ui';
export const MyPage = () => {
const [error, setError] = useState<string | null>(null);
if (error) {
return <Alert title="Error" severity="error">{error}</Alert>;
}
return <div>Content</div>;
};
Data Fetching
import { useAsync } from 'react-use';
import { getBackendSrv } from '@grafana/runtime';
export const MyPage = () => {
const { value, loading, error } = useAsync(async () => {
const response = await getBackendSrv().fetch({
url: '/api/plugins/myorg-myapp-app/resources/data',
});
return response.data;
});
// Handle loading and error states
};
Resources