插件开发
Grafana 提供强大的插件系统,支持开发自定义面板、数据源和应用插件。
插件类型
插件分类
| 类型 | 说明 | 用途 |
|---|---|---|
| Panel | 面板插件 | 自定义可视化 |
| DataSource | 数据源插件 | 新数据源集成 |
| App | 应用插件 | 完整应用集成 |
| Transformation | 转换插件 | 数据处理 |
插件结构
目录结构
my-plugin/
├── src/
│ ├── Plugin.tsx # 插件入口
│ ├── components/
│ │ ├── SimplePanel.tsx # 面板组件
│ │ └── SimpleEditor.tsx # 配置编辑器
│ └── module.ts # 模块定义
├── dist/ # 编译输出
├── plugin.json # 插件元数据
├── package.json # npm 配置
├── tsconfig.json # TypeScript 配置
└── webpack.config.ts # Webpack 配置plugin.json 配置
json
{
"id": "myorg-simple-panel",
"type": "panel",
"name": "Simple Panel",
"info": {
"description": "Simple panel plugin",
"author": {
"name": "My Organization"
},
"keywords": ["panel", "simple"],
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"version": "1.0.0"
},
"dependencies": {
"grafanaDependency": ">=10.0.0",
"plugins": []
}
}面板插件开发
基础面板组件
typescript
import React, { PureComponent } from 'react';
import { PanelProps } from '@grafana/data';
import { SimpleOptions } from './types';
interface Props extends PanelProps<SimpleOptions> {}
export class SimplePanel extends PureComponent<Props> {
render() {
const { data, width, height, options } = this.props;
return (
<div style={{ width, height }}>
<h3>{options.title}</h3>
<p>Value: {data.series[0]?.fields[0]?.values.get(0)}</p>
</div>
);
}
}配置选项
typescript
export interface SimpleOptions {
title: string;
showValue: boolean;
color: string;
}
export const defaults: SimpleOptions = {
title: 'Simple Panel',
showValue: true,
color: '#00ff00',
};配置编辑器
typescript
import React, { PureComponent } from 'react';
import { PanelOptionsEditorProps } from '@grafana/data';
import { SimpleOptions } from './types';
export class SimpleEditor extends PureComponent<PanelOptionsEditorProps<SimpleOptions>> {
onTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.props.onOptionsChange({ ...this.props.options, title: event.target.value });
};
onColorChange = (color: string) => {
this.props.onOptionsChange({ ...this.props.options, color: color });
};
render() {
const { options } = this.props;
return (
<div className="editor-section">
<div className="editor-row">
<label>Title</label>
<input
type="text"
value={options.title}
onChange={this.onTitleChange}
/>
</div>
<div className="editor-row">
<label>Color</label>
<input
type="color"
value={options.color}
onChange={(e) => this.onColorChange(e.target.value)}
/>
</div>
</div>
);
}
}模块注册
typescript
import { PanelPlugin } from '@grafana/data';
import { SimplePanel } from './components/SimplePanel';
import { SimpleEditor } from './components/SimpleEditor';
import { defaults } from './types';
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel)
.setDefaults(defaults)
.setEditor(SimpleEditor);数据源插件开发
数据源组件
typescript
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
import { MyDataSourceOptions, MyQuery } from './types';
export class MyDataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
super(instanceSettings);
this.url = instanceSettings.url;
}
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
const { targets } = options;
const data = [];
for (const target of targets) {
const response = await this.doRequest(target);
data.push(response);
}
return { data };
}
async doRequest(query: MyQuery): Promise<any> {
const response = await fetch(`${this.url}/api/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query),
});
return response.json();
}
async testDatasource(): Promise<any> {
try {
const response = await fetch(`${this.url}/health`);
if (response.ok) {
return { status: 'success', message: 'Data source is working' };
}
return { status: 'error', message: 'Data source is not working' };
} catch (error) {
return { status: 'error', message: error.message };
}
}
}配置编辑器
typescript
import React, { PureComponent } from 'react';
import { DataSourceSettings } from '@grafana/data';
import { MyDataSourceOptions } from './types';
interface Props {
options: DataSourceSettings<MyDataSourceOptions>;
onOptionsChange: (options: DataSourceSettings<MyDataSourceOptions>) => void;
}
export class ConfigEditor extends PureComponent<Props> {
onUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { options, onOptionsChange } = this.props;
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
url: event.target.value,
},
});
};
render() {
const { options } = this.props;
return (
<div className="gf-form-group">
<div className="gf-form">
<label className="gf-form-label">URL</label>
<input
className="gf-form-input"
type="text"
value={options.jsonData.url || ''}
onChange={this.onUrlChange}
placeholder="http://localhost:8080"
/>
</div>
</div>
);
}
}查询编辑器
typescript
import React, { PureComponent } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { MyDataSource } from './datasource';
import { MyQuery, MyDataSourceOptions } from './types';
type Props = QueryEditorProps<MyDataSource, MyQuery, MyDataSourceOptions>;
export class QueryEditor extends PureComponent<Props> {
onQueryTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { query, onChange, onRunQuery } = this.props;
onChange({ ...query, queryText: event.target.value });
onRunQuery();
};
render() {
const { query } = this.props;
return (
<div className="gf-form">
<label className="gf-form-label">Query</label>
<input
className="gf-form-input"
type="text"
value={query.queryText || ''}
onChange={this.onQueryTextChange}
placeholder="Enter query"
/>
</div>
);
}
}模块注册
typescript
import { DataSourcePlugin } from '@grafana/data';
import { MyDataSource } from './datasource';
import { ConfigEditor } from './components/ConfigEditor';
import { QueryEditor } from './components/QueryEditor';
export const plugin = new DataSourcePlugin(MyDataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor);应用插件开发
应用入口
typescript
import { AppPlugin } from '@grafana/data';
import { AppComponent } from './components/AppComponent';
import { AppConfigCtrl } from './components/AppConfigCtrl';
export const plugin = new AppPlugin()
.setRootPage(AppComponent)
.setConfigPage(AppConfigCtrl);应用组件
typescript
import React, { PureComponent } from 'react';
import { AppRootProps } from '@grafana/data';
export class AppComponent extends PureComponent<AppRootProps> {
render() {
const { path } = this.props;
return (
<div className="page-container">
<h2>My App Plugin</h2>
<p>Current path: {path}</p>
</div>
);
}
}开发环境设置
初始化项目
bash
# 使用 Grafana 插件工具创建项目
npx @grafana/create-plugin@latest
# 或手动创建
mkdir my-plugin && cd my-plugin
npm init -y
npm install @grafana/data @grafana/ui @grafana/runtime
npm install --save-dev webpack webpack-cli typescript ts-loaderpackage.json 配置
json
{
"name": "my-plugin",
"version": "1.0.0",
"scripts": {
"build": "webpack -c webpack.config.ts",
"dev": "webpack -c webpack.config.ts --watch",
"test": "jest"
},
"dependencies": {
"@grafana/data": "^10.0.0",
"@grafana/ui": "^10.0.0",
"@grafana/runtime": "^10.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
}
}webpack.config.ts
typescript
import { Configuration } from 'webpack';
const config: Configuration = {
entry: './src/module.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'module.js',
libraryTarget: 'amd',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
'react',
'react-dom',
'@grafana/data',
'@grafana/ui',
'@grafana/runtime',
],
};
export default config;tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}测试和发布
单元测试
typescript
import { SimplePanel } from './SimplePanel';
import { render } from '@testing-library/react';
describe('SimplePanel', () => {
it('renders correctly', () => {
const props = {
data: { series: [] },
width: 500,
height: 300,
options: { title: 'Test' },
};
const { getByText } = render(<SimplePanel {...props} />);
expect(getByText('Test')).toBeInTheDocument();
});
});本地测试
bash
# 编译插件
npm run build
# 复制到 Grafana 插件目录
cp -r dist /var/lib/grafana/plugins/myorg-simple-panel
# 重启 Grafana
docker restart grafana发布插件
bash
# 打包插件
grafana-toolkit plugin:build
# 验证插件
grafana-toolkit plugin:sign
# 上传到 Grafana 插件市场
# 或发布到私有仓库最佳实践
- 遵循 Grafana 设计规范:使用 Grafana UI 组件库
- 性能优化:避免频繁渲染,使用 React.memo
- 错误处理:捕获并显示错误信息
- 国际化:支持多语言显示
- 类型安全:使用 TypeScript 定义类型
- 文档完善:提供使用文档和示例
- 版本管理:遵循语义化版本规范
- 测试覆盖:编写单元测试和集成测试
参考:https://grafana.com/docs/grafana/latest/developers/plugins/