Skip to content

插件开发 ​

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-loader

package.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 插件市场
# 或发布到私有仓库

最佳实践 ​

  1. 遵循 Grafana 设计规范:使用 Grafana UI 组件库
  2. 性能优化:避免频繁渲染,使用 React.memo
  3. 错误处理:捕获并显示错误信息
  4. 国际化:支持多语言显示
  5. 类型安全:使用 TypeScript 定义类型
  6. 文档完善:提供使用文档和示例
  7. 版本管理:遵循语义化版本规范
  8. 测试覆盖:编写单元测试和集成测试

参考:https://grafana.com/docs/grafana/latest/developers/plugins/