概述

Grafana插件系统是其强大功能的核心,允许开发者扩展Grafana的功能,创建自定义数据源、面板、应用程序等。本教程将深入介绍Grafana插件开发的各个方面。

学习目标

  • 理解Grafana插件架构和类型
  • 掌握插件开发环境搭建
  • 学会开发数据源插件
  • 学会开发面板插件
  • 学会开发应用插件
  • 了解插件发布和分发

插件类型与架构

1. 插件类型概览

from enum import Enum
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
import json
import uuid
from datetime import datetime

class PluginType(Enum):
    """插件类型枚举"""
    DATASOURCE = "datasource"
    PANEL = "panel"
    APP = "app"
    RENDERER = "renderer"
    SECRETSMANAGER = "secretsmanager"

class PluginState(Enum):
    """插件状态枚举"""
    ALPHA = "alpha"
    BETA = "beta"
    STABLE = "stable"
    DEPRECATED = "deprecated"

class PluginSignatureStatus(Enum):
    """插件签名状态枚举"""
    INTERNAL = "internal"
    VALID = "valid"
    INVALID = "invalid"
    MODIFIED = "modified"
    UNSIGNED = "unsigned"

@dataclass
class PluginInfo:
    """插件信息数据类"""
    id: str
    name: str
    type: PluginType
    version: str
    description: str
    author: str
    keywords: List[str]
    screenshots: List[str]
    logos: Dict[str, str]
    links: List[Dict[str, str]]
    dependencies: Dict[str, str]
    state: PluginState
    signature: PluginSignatureStatus
    updated: str

class PluginManager:
    """插件管理器"""
    
    def __init__(self):
        self.plugins = {}
        self.plugin_configs = {}
        self.plugin_templates = {}
    
    def get_plugin_types(self) -> Dict[str, Dict]:
        """获取支持的插件类型"""
        return {
            "datasource": {
                "description": "数据源插件,用于连接和查询外部数据源",
                "capabilities": [
                    "query", "annotations", "metrics", "logs", "streaming"
                ],
                "examples": [
                    "prometheus", "mysql", "elasticsearch", "influxdb"
                ]
            },
            "panel": {
                "description": "面板插件,用于创建自定义可视化组件",
                "capabilities": [
                    "visualization", "editor", "field_options", "transformations"
                ],
                "examples": [
                    "graph", "table", "stat", "gauge", "heatmap"
                ]
            },
            "app": {
                "description": "应用插件,提供完整的应用程序功能",
                "capabilities": [
                    "pages", "config", "includes", "dependencies"
                ],
                "examples": [
                    "kubernetes-app", "worldmap-panel", "clock-panel"
                ]
            },
            "renderer": {
                "description": "渲染器插件,用于图像渲染和导出",
                "capabilities": [
                    "png_export", "pdf_export", "custom_rendering"
                ],
                "examples": [
                    "image-renderer", "phantomjs-renderer"
                ]
            }
        }
    
    def create_plugin_scaffold(self, plugin_type: PluginType, 
                             plugin_name: str, plugin_id: str) -> Dict:
        """创建插件脚手架"""
        scaffold = {
            "plugin_json": self._generate_plugin_json(plugin_type, plugin_name, plugin_id),
            "package_json": self._generate_package_json(plugin_name, plugin_id),
            "webpack_config": self._generate_webpack_config(),
            "tsconfig": self._generate_tsconfig(),
            "source_structure": self._generate_source_structure(plugin_type),
            "build_scripts": self._generate_build_scripts(),
            "documentation": self._generate_documentation_template(plugin_type)
        }
        
        return scaffold
    
    def _generate_plugin_json(self, plugin_type: PluginType, 
                            plugin_name: str, plugin_id: str) -> Dict:
        """生成plugin.json配置"""
        base_config = {
            "type": plugin_type.value,
            "name": plugin_name,
            "id": plugin_id,
            "info": {
                "description": f"Custom {plugin_type.value} plugin",
                "author": {
                    "name": "Your Name",
                    "url": "https://your-website.com"
                },
                "keywords": [plugin_type.value, "grafana", "plugin"],
                "logos": {
                    "small": "img/logo.svg",
                    "large": "img/logo.svg"
                },
                "links": [
                    {"name": "Website", "url": "https://github.com/your-org/your-plugin"},
                    {"name": "License", "url": "https://github.com/your-org/your-plugin/blob/master/LICENSE"}
                ],
                "screenshots": [],
                "version": "1.0.0",
                "updated": datetime.now().strftime("%Y-%m-%d")
            },
            "dependencies": {
                "grafanaDependency": ">=8.0.0",
                "plugins": []
            }
        }
        
        # 根据插件类型添加特定配置
        if plugin_type == PluginType.DATASOURCE:
            base_config.update({
                "backend": True,
                "executable": f"{plugin_id}",
                "queryOptions": {
                    "maxDataPoints": True,
                    "minInterval": True
                },
                "alerting": True,
                "annotations": True,
                "metrics": True,
                "logs": False,
                "streaming": False
            })
        elif plugin_type == PluginType.PANEL:
            base_config.update({
                "skipDataQuery": False
            })
        elif plugin_type == PluginType.APP:
            base_config.update({
                "includes": [
                    {
                        "type": "page",
                        "name": "Main",
                        "path": "/plugins/" + plugin_id,
                        "role": "Viewer",
                        "addToNav": True,
                        "defaultNav": True
                    }
                ]
            })
        
        return base_config
    
    def _generate_package_json(self, plugin_name: str, plugin_id: str) -> Dict:
        """生成package.json配置"""
        return {
            "name": plugin_id,
            "version": "1.0.0",
            "description": f"Grafana {plugin_name} plugin",
            "scripts": {
                "build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
                "dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
                "test": "jest --watch --onlyChanged",
                "test:ci": "jest --passWithNoTests --maxWorkers 4",
                "typecheck": "tsc --noEmit",
                "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
                "lint:fix": "npm run lint -- --fix",
                "e2e": "npm exec playwright test",
                "e2e:update": "npm exec playwright test --update-snapshots",
                "server": "docker-compose up --build",
                "sign": "npx --yes @grafana/sign-plugin@latest"
            },
            "author": "Your Name",
            "license": "Apache-2.0",
            "devDependencies": {
                "@grafana/data": "latest",
                "@grafana/runtime": "latest",
                "@grafana/ui": "latest",
                "@grafana/toolkit": "latest",
                "@types/lodash": "latest",
                "typescript": "latest",
                "webpack": "latest",
                "@swc/core": "latest",
                "@swc/jest": "latest",
                "jest": "latest",
                "eslint": "latest"
            },
            "engines": {
                "node": ">=16"
            }
        }
    
    def _generate_webpack_config(self) -> str:
        """生成Webpack配置"""
        return """
import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import grafanaConfig from './.config/webpack/webpack.config';

const config = async (env): Promise<Configuration> => {
  const baseConfig = await grafanaConfig(env);

  return merge(baseConfig, {
    // Add custom webpack config here
  });
};

export default config;
"""
    
    def _generate_tsconfig(self) -> Dict:
        """生成TypeScript配置"""
        return {
            "compilerOptions": {
                "target": "ES2015",
                "lib": ["ES2015", "DOM"],
                "allowJs": False,
                "skipLibCheck": True,
                "esModuleInterop": True,
                "allowSyntheticDefaultImports": True,
                "strict": True,
                "forceConsistentCasingInFileNames": True,
                "module": "ESNext",
                "moduleResolution": "node",
                "resolveJsonModule": True,
                "isolatedModules": True,
                "noEmit": True,
                "jsx": "react-jsx",
                "declaration": True,
                "types": ["jest", "node"]
            },
            "include": ["src", "types"],
            "exclude": ["dist", "node_modules"]
        }
    
    def _generate_source_structure(self, plugin_type: PluginType) -> Dict:
        """生成源码结构"""
        base_structure = {
            "src/": {
                "module.ts": "插件主模块",
                "plugin.json": "插件配置文件",
                "README.md": "插件说明文档",
                "img/": {
                    "logo.svg": "插件图标"
                },
                "types.ts": "TypeScript类型定义"
            },
            ".config/": {
                "webpack/": {
                    "webpack.config.ts": "Webpack配置"
                }
            },
            "tests/": {
                "__mocks__/": "测试模拟文件",
                "datasource.test.ts": "数据源测试"
            }
        }
        
        if plugin_type == PluginType.DATASOURCE:
            base_structure["src/"].update({
                "datasource.ts": "数据源实现",
                "ConfigEditor.tsx": "配置编辑器组件",
                "QueryEditor.tsx": "查询编辑器组件",
                "VariableQueryEditor.tsx": "变量查询编辑器",
                "AnnotationQueryEditor.tsx": "注释查询编辑器"
            })
        elif plugin_type == PluginType.PANEL:
            base_structure["src/"].update({
                "SimplePanel.tsx": "面板组件",
                "SimpleEditor.tsx": "面板编辑器",
                "FieldDisplay.tsx": "字段显示组件"
            })
        elif plugin_type == PluginType.APP:
            base_structure["src/"].update({
                "components/": {
                    "App.tsx": "应用主组件",
                    "ConfigEditor.tsx": "配置编辑器",
                    "ExamplePage.tsx": "示例页面"
                },
                "pages/": {
                    "PageOne.tsx": "页面一",
                    "PageTwo.tsx": "页面二"
                }
            })
        
        return base_structure
    
    def _generate_build_scripts(self) -> Dict:
        """生成构建脚本"""
        return {
            "docker-compose.yml": """
version: '3.8'
services:
  grafana:
    container_name: 'grafana-dev'
    build:
      context: ./.config
      args:
        grafana_image: grafana/grafana:8.5.0
        grafana_version: 8.5.0
    ports:
      - 3000:3000/tcp
    volumes:
      - ./dist:/var/lib/grafana/plugins/your-plugin
      - ./provisioning:/etc/grafana/provisioning
    environment:
      - TERM=linux
      - GF_LOG_LEVEL=debug
      - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=your-plugin-id
""",
            "Makefile": """
.PHONY: build clean install test

build:
	npm run build

dev:
	npm run dev

clean:
	rm -rf dist/

install:
	npm install

test:
	npm run test:ci

lint:
	npm run lint

sign:
	npm run sign

server:
	npm run server
"""
        }
    
    def _generate_documentation_template(self, plugin_type: PluginType) -> str:
        """生成文档模板"""
        return f"""
# {plugin_type.value.title()} Plugin

## Overview

This is a custom Grafana {plugin_type.value} plugin.

## Installation

### From Grafana UI

1. Go to Configuration > Plugins
2. Search for your plugin
3. Click Install

### Manual Installation

1. Download the plugin
2. Extract to Grafana plugins directory
3. Restart Grafana

## Configuration

### Plugin Configuration

Describe how to configure the plugin...

### Data Source Configuration (if applicable)

Describe data source specific configuration...

## Usage

Describe how to use the plugin...

## Development

### Prerequisites

- Node.js 16+
- npm or yarn
- Docker (optional)

### Setup

```bash
npm install
npm run dev

Building

npm run build

Testing

npm run test

Contributing

Contributions are welcome! Please read the contributing guidelines.

License

Apache 2.0 License “””

def validate_plugin_structure(self, plugin_path: str) -> Dict:
    """验证插件结构"""
    validation_result = {
        "valid": True,
        "errors": [],
        "warnings": [],
        "suggestions": []
    }

    required_files = [
        "plugin.json",
        "module.ts",
        "package.json"
    ]

    recommended_files = [
        "README.md",
        "CHANGELOG.md",
        "LICENSE",
        "img/logo.svg"
    ]

    # 检查必需文件
    for file in required_files:
        # 这里应该检查文件是否存在
        # if not os.path.exists(os.path.join(plugin_path, file)):
        #     validation_result["errors"].append(f"Missing required file: {file}")
        #     validation_result["valid"] = False
        pass

    # 检查推荐文件
    for file in recommended_files:
        # if not os.path.exists(os.path.join(plugin_path, file)):
        #     validation_result["warnings"].append(f"Missing recommended file: {file}")
        pass

    # 添加建议
    validation_result["suggestions"].extend([
        "Add comprehensive unit tests",
        "Include integration tests",
        "Add proper error handling",
        "Implement proper logging",
        "Add performance monitoring"
    ])

    return validation_result

def generate_plugin_manifest(self, plugin_info: PluginInfo) -> Dict:
    """生成插件清单"""
    manifest = {
        "plugin": {
            "id": plugin_info.id,
            "name": plugin_info.name,
            "type": plugin_info.type.value,
            "version": plugin_info.version,
            "description": plugin_info.description,
            "author": plugin_info.author,
            "keywords": plugin_info.keywords,
            "state": plugin_info.state.value,
            "signature": plugin_info.signature.value,
            "updated": plugin_info.updated
        },
        "assets": {
            "screenshots": plugin_info.screenshots,
            "logos": plugin_info.logos
        },
        "links": plugin_info.links,
        "dependencies": plugin_info.dependencies,
        "build_info": {
            "build_timestamp": datetime.now().isoformat(),
            "build_hash": f"build_{uuid.uuid4().hex[:8]}",
            "grafana_version": ">=8.0.0"
        }
    }

    return manifest

使用示例

plugin_manager = PluginManager()

获取插件类型信息

plugin_types = plugin_manager.get_plugin_types() print(“支持的插件类型:”) for ptype, info in plugin_types.items(): print(f”- {ptype}: {info[‘description’]}“)

创建数据源插件脚手架

scaffold = plugin_manager.create_plugin_scaffold( PluginType.DATASOURCE, “My Custom DataSource”, “mycustom-datasource” )

print(“\n数据源插件脚手架已创建”) print(f”Plugin JSON: {json.dumps(scaffold[‘plugin_json’], indent=2)[:200]}…“)

创建插件信息

plugin_info = PluginInfo( id=“mycustom-datasource”, name=“My Custom DataSource”, type=PluginType.DATASOURCE, version=“1.0.0”, description=“A custom data source plugin for Grafana”, author=“Your Name”, keywords=[“datasource”, “custom”, “api”], screenshots=[“img/screenshot1.png”, “img/screenshot2.png”], logos={“small”: “img/logo.svg”, “large”: “img/logo.svg”}, links=[ {“name”: “GitHub”, “url”: “https://github.com/your-org/your-plugin”}, {“name”: “Documentation”, “url”: “https://your-plugin-docs.com”} ], dependencies={“grafanaDependency”: “>=8.0.0”}, state=PluginState.BETA, signature=PluginSignatureStatus.UNSIGNED, updated=datetime.now().strftime(“%Y-%m-%d”) )

生成插件清单

manifest = plugin_manager.generate_plugin_manifest(plugin_info) print(f”\n插件清单已生成: {manifest[‘plugin’][‘name’]} v{manifest[‘plugin’][‘version’]}“)


## 数据源插件开发

### 1. 数据源插件架构

```python
class DataSourcePlugin:
    """数据源插件基类"""
    
    def __init__(self, plugin_id: str, name: str):
        self.plugin_id = plugin_id
        self.name = name
        self.config = {}
        self.query_types = []
        self.capabilities = set()
    
    def create_datasource_implementation(self) -> str:
        """创建数据源实现代码"""
        return f"""
import {{
  DataQueryRequest,
  DataQueryResponse,
  DataSourceApi,
  DataSourceInstanceSettings,
  MutableDataFrame,
  FieldType,
}} from '@grafana/data';

import {{ MyQuery, MyDataSourceOptions, DEFAULT_QUERY }} from './types';

export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {{
  url?: string;
  
  constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {{
    super(instanceSettings);
    this.url = instanceSettings.url;
  }}

  async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {{
    const {{ range }} = options;
    const from = range!.from.valueOf();
    const to = range!.to.valueOf();

    const promises = options.targets.map(async (target) => {{
      const query = {{ ...DEFAULT_QUERY, ...target }};
      
      if (query.hide) {{
        return new MutableDataFrame();
      }}

      // 实现查询逻辑
      const data = await this.doRequest(query, from, to);
      
      return new MutableDataFrame({{
        refId: query.refId,
        fields: [
          {{ name: 'Time', type: FieldType.time, values: data.timestamps }},
          {{ name: 'Value', type: FieldType.number, values: data.values }},
        ],
      }});
    }});

    return Promise.all(promises).then((data) => ({{ data }}));
  }}

  async doRequest(query: MyQuery, from: number, to: number) {{
    // 实现具体的数据请求逻辑
    const response = await fetch(`${{this.url}}/api/query`, {{
      method: 'POST',
      headers: {{
        'Content-Type': 'application/json',
      }},
      body: JSON.stringify({{
        query: query.queryText,
        from,
        to,
      }}),
    }});
    
    return response.json();
  }}

  async testDatasource() {{
    // 实现数据源连接测试
    try {{
      const response = await fetch(`${{this.url}}/api/health`);
      if (response.ok) {{
        return {{
          status: 'success',
          message: 'Data source is working',
        }};
      }}
      throw new Error('Health check failed');
    }} catch (error) {{
      return {{
        status: 'error',
        message: `Cannot connect to API: ${{error}}`,
      }};
    }}
  }}

  async metricFindQuery(query: string): Promise<any[]> {{
    // 实现变量查询
    const response = await fetch(`${{this.url}}/api/search?q=${{encodeURIComponent(query)}}`);
    const data = await response.json();
    
    return data.map((item: any) => ({{ text: item.name, value: item.id }}));
  }}

  getDefaultQuery(): Partial<MyQuery> {{
    return DEFAULT_QUERY;
  }}
}}
"""
    
    def create_config_editor(self) -> str:
        """创建配置编辑器组件"""
        return f"""
import React, {{ ChangeEvent, PureComponent }} from 'react';
import {{ LegacyForms }} from '@grafana/ui';
import {{ DataSourcePluginOptionsEditorProps }} from '@grafana/data';
import {{ MyDataSourceOptions, MySecureJsonData }} from '../types';

const {{ SecretFormField, FormField }} = LegacyForms;

interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions> {{}}

interface State {{}}

export class ConfigEditor extends PureComponent<Props, State> {{
  onURLChange = (event: ChangeEvent<HTMLInputElement>) => {{
    const {{ onOptionsChange, options }} = this.props;
    const jsonData = {{
      ...options.jsonData,
      url: event.target.value,
    }};
    onOptionsChange({{ ...options, jsonData }});
  }};

  onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {{
    const {{ onOptionsChange, options }} = this.props;
    onOptionsChange({{
      ...options,
      secureJsonData: {{
        apiKey: event.target.value,
      }},
    }});
  }};

  onResetAPIKey = () => {{
    const {{ onOptionsChange, options }} = this.props;
    onOptionsChange({{
      ...options,
      secureJsonFields: {{
        ...options.secureJsonFields,
        apiKey: false,
      }},
      secureJsonData: {{
        ...options.secureJsonData,
        apiKey: '',
      }},
    }});
  }};

  render() {{
    const {{ options }} = this.props;
    const {{ jsonData, secureJsonFields }} = options;
    const secureJsonData = (options.secureJsonData || {{}}) as MySecureJsonData;

    return (
      <div className="gf-form-group">
        <div className="gf-form">
          <FormField
            label="URL"
            labelWidth={{6}}
            inputWidth={{20}}
            onChange={{this.onURLChange}}
            value={{jsonData.url || ''}}
            placeholder="http://localhost:8080"
          />
        </div>

        <div className="gf-form-inline">
          <div className="gf-form">
            <SecretFormField
              isConfigured={{(secureJsonFields && secureJsonFields.apiKey) as boolean}}
              value={{secureJsonData.apiKey || ''}}
              label="API Key"
              placeholder="Enter API key"
              labelWidth={{6}}
              inputWidth={{20}}
              onReset={{this.onResetAPIKey}}
              onChange={{this.onAPIKeyChange}}
            />
          </div>
        </div>
      </div>
    );
  }}
}}
"""
    
    def create_query_editor(self) -> str:
        """创建查询编辑器组件"""
        return f"""
import React, {{ ChangeEvent, PureComponent }} from 'react';
import {{ LegacyForms }} from '@grafana/ui';
import {{ QueryEditorProps }} from '@grafana/data';
import {{ DataSource }} from '../datasource';
import {{ MyDataSourceOptions, MyQuery }} from '../types';

const {{ FormField }} = LegacyForms;

type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;

export class QueryEditor extends PureComponent<Props> {{
  onQueryTextChange = (event: ChangeEvent<HTMLInputElement>) => {{
    const {{ onChange, query }} = this.props;
    onChange({{ ...query, queryText: event.target.value }});
  }};

  onConstantChange = (event: ChangeEvent<HTMLInputElement>) => {{
    const {{ onChange, query, onRunQuery }} = this.props;
    onChange({{ ...query, constant: parseFloat(event.target.value) }});
    onRunQuery();
  }};

  render() {{
    const {{ query }} = this.props;
    const {{ queryText, constant }} = query;

    return (
      <div className="gf-form">
        <FormField
          width={{4}}
          value={{constant}}
          onChange={{this.onConstantChange}}
          label="Constant"
          type="number"
          step="0.1"
        />
        <FormField
          labelWidth={{8}}
          value={{queryText || ''}}
          onChange={{this.onQueryTextChange}}
          label="Query Text"
          tooltip="Enter your query here"
        />
      </div>
    );
  }}
}}
"""
    
    def create_types_definition(self) -> str:
        """创建类型定义"""
        return f"""
import {{ DataQuery, DataSourceJsonData }} from '@grafana/data';

export interface MyQuery extends DataQuery {{
  queryText?: string;
  constant: number;
}}

export const DEFAULT_QUERY: Partial<MyQuery> = {{
  constant: 6.5,
}};

export interface MyDataSourceOptions extends DataSourceJsonData {{
  url?: string;
  timeout?: number;
}}

export interface MySecureJsonData {{
  apiKey?: string;
}}

export interface MyVariableQuery {{
  namespace: string;
  rawQuery: string;
}}
"""
    
    def create_module_file(self) -> str:
        """创建模块文件"""
        return f"""
import {{ DataSourcePlugin }} from '@grafana/data';
import {{ DataSource }} from './datasource';
import {{ ConfigEditor }} from './components/ConfigEditor';
import {{ QueryEditor }} from './components/QueryEditor';
import {{ MyQuery, MyDataSourceOptions }} from './types';

export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
  .setConfigEditor(ConfigEditor)
  .setQueryEditor(QueryEditor);
"""
    
    def create_backend_implementation(self) -> str:
        """创建后端实现(Go语言)"""
        return f"""
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
	"github.com/grafana/grafana-plugin-sdk-go/data"
)

// Make sure Datasource implements required interfaces.
var (
	_ backend.QueryDataHandler      = (*Datasource)(nil)
	_ backend.CheckHealthHandler    = (*Datasource)(nil)
	_ instancemgmt.InstanceDisposer = (*Datasource)(nil)
)

// NewDatasource creates a new datasource instance.
func NewDatasource(_ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {{
	return &Datasource{{}}, nil
}}

// Datasource is an example datasource which can respond to data queries.
type Datasource struct {{}}

// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
// created. As soon as datasource settings change detected by SDK old datasource instance will
// be disposed and a new one will be created using NewSampleDatasource factory function.
func (d *Datasource) Dispose() {{
	// Clean up datasource instance resources.
}}

// QueryData handles multiple queries and returns multiple responses.
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {{
	log.DefaultLogger.Info("QueryData called", "request", req)

	// create response struct
	response := backend.NewQueryDataResponse()

	// loop over queries and execute them individually.
	for _, q := range req.Queries {{
		res := d.query(ctx, req.PluginContext, q)

		// save the response in a hashmap
		// based on with RefID as identifier
		response.Responses[q.RefID] = res
	}}

	return response, nil
}}

type queryModel struct {{
	QueryText string  `json:"queryText"`
	Constant  float64 `json:"constant"`
}}

func (d *Datasource) query(_ context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {{
	response := backend.DataResponse{{}}

	// Unmarshal the JSON into our queryModel.
	var qm queryModel

	response.Error = json.Unmarshal(query.JSON, &qm)
	if response.Error != nil {{
		return response
	}}

	// create data frame response.
	frame := data.NewFrame("response")

	// add fields.
	frame.Fields = append(frame.Fields,
		data.NewField("time", nil, []time.Time{{query.TimeRange.From, query.TimeRange.To}}),
		data.NewField("values", nil, []float64{{qm.Constant, qm.Constant * 2}}),
	)

	// add the frames to the response.
	response.Frames = append(response.Frames, frame)

	return response
}}

// CheckHealth handles health checks sent from Grafana to the plugin.
func (d *Datasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {{
	log.DefaultLogger.Info("CheckHealth called", "request", req)

	var status = backend.HealthStatusOk
	var message = "Data source is working"

	// Perform health check logic here
	// You can test connectivity to your data source

	return &backend.CheckHealthResult{{
		Status:  status,
		Message: message,
	}}, nil
}}

func main() {{
	// Start listening to requests sent from Grafana. This call is blocking so
	// it won't finish until Grafana shuts down the process or the plugin choose
	// to exit by itself using os.Exit. Manage automatically manages life cycle
	// of datasource instances. It accepts datasource instance factory as first
	// argument. This factory will be called when new datasource instance requested
	// by Grafana.
	if err := backend.Manage("my-datasource-plugin", NewDatasource, backend.ManageOpts{{}}); err != nil {{
		log.DefaultLogger.Error(err.Error())
	}}
}}
"""
    
    def generate_test_suite(self) -> str:
        """生成测试套件"""
        return f"""
import {{ DataSource }} from '../datasource';
import {{ createMockInstanceSettings }} from './__mocks__/datasource';
import {{ of }} from 'rxjs';

const mockInstanceSettings = createMockInstanceSettings();

describe('DataSource', () => {{
  let datasource: DataSource;

  beforeEach(() => {{
    datasource = new DataSource(mockInstanceSettings);
  }});

  describe('query', () => {{
    it('should return data frames', async () => {{
      const query = {{
        targets: [
          {{
            refId: 'A',
            queryText: 'test query',
            constant: 10,
          }},
        ],
        range: {{
          from: new Date('2021-01-01'),
          to: new Date('2021-01-02'),
        }},
      }} as any;

      const result = await datasource.query(query);
      
      expect(result.data).toHaveLength(1);
      expect(result.data[0].fields).toHaveLength(2);
      expect(result.data[0].fields[0].name).toBe('Time');
      expect(result.data[0].fields[1].name).toBe('Value');
    }});
  }});

  describe('testDatasource', () => {{
    it('should return success status', async () => {{
      // Mock successful API response
      global.fetch = jest.fn(() =>
        Promise.resolve({{
          ok: true,
          json: () => Promise.resolve({{ status: 'ok' }}),
        }})
      ) as jest.Mock;

      const result = await datasource.testDatasource();
      
      expect(result.status).toBe('success');
      expect(result.message).toBe('Data source is working');
    }});

    it('should return error status on failure', async () => {{
      // Mock failed API response
      global.fetch = jest.fn(() =>
        Promise.reject(new Error('Network error'))
      ) as jest.Mock;

      const result = await datasource.testDatasource();
      
      expect(result.status).toBe('error');
      expect(result.message).toContain('Cannot connect to API');
    }});
  }});

  describe('metricFindQuery', () => {{
    it('should return metric options', async () => {{
      const mockResponse = [
        {{ name: 'metric1', id: 'metric1' }},
        {{ name: 'metric2', id: 'metric2' }},
      ];

      global.fetch = jest.fn(() =>
        Promise.resolve({{
          json: () => Promise.resolve(mockResponse),
        }})
      ) as jest.Mock;

      const result = await datasource.metricFindQuery('test');
      
      expect(result).toHaveLength(2);
      expect(result[0]).toEqual({{ text: 'metric1', value: 'metric1' }});
      expect(result[1]).toEqual({{ text: 'metric2', value: 'metric2' }});
    }});
  }});
}});
"""

# 使用示例
datasource_plugin = DataSourcePlugin("mycustom-datasource", "My Custom DataSource")

# 生成数据源实现代码
datasource_code = datasource_plugin.create_datasource_implementation()
print("数据源实现代码已生成")

# 生成配置编辑器
config_editor_code = datasource_plugin.create_config_editor()
print("配置编辑器代码已生成")

# 生成查询编辑器
query_editor_code = datasource_plugin.create_query_editor()
print("查询编辑器代码已生成")

# 生成类型定义
types_code = datasource_plugin.create_types_definition()
print("类型定义代码已生成")

# 生成模块文件
module_code = datasource_plugin.create_module_file()
print("模块文件代码已生成")

# 生成后端实现
backend_code = datasource_plugin.create_backend_implementation()
print("后端实现代码已生成")

# 生成测试套件
test_code = datasource_plugin.generate_test_suite()
print("测试套件代码已生成")

面板插件开发

1. 面板插件架构

class PanelPlugin:
    """面板插件基类"""
    
    def __init__(self, plugin_id: str, name: str):
        self.plugin_id = plugin_id
        self.name = name
        self.panel_options = {}
        self.field_options = {}
        self.supported_data_formats = []
    
    def create_panel_component(self) -> str:
        """创建面板组件"""
        return f"""
import React from 'react';
import {{ PanelProps }} from '@grafana/data';
import {{ SimpleOptions }} from 'types';
import {{ css, cx }} from '@emotion/css';
import {{ useStyles2, useTheme2 }} from '@grafana/ui';

interface Props extends PanelProps<SimpleOptions> {{}}

const getStyles = () => {{
  return {{
    wrapper: css`
      font-family: Open Sans;
      position: relative;
    `,
    svg: css`
      position: absolute;
      top: 0;
      left: 0;
    `,
    textBox: css`
      position: absolute;
      bottom: 0;
      left: 0;
      padding: 10px;
    `,
  }};
}};

export const SimplePanel: React.FC<Props> = ({{ options, data, width, height }}) => {{
  const theme = useTheme2();
  const styles = useStyles2(getStyles);
  
  // 处理数据
  const frame = data.series[0];
  const values = frame?.fields.find((field) => field.type === 'number')?.values.toArray() || [];
  const lastValue = values[values.length - 1] || 0;
  
  return (
    <div
      className={{cx(
        styles.wrapper,
        css`
          width: ${{width}}px;
          height: ${{height}}px;
        `
      )}}
    >
      <svg
        className={{styles.svg}}
        width={{width}}
        height={{height}}
        xmlns="http://www.w3.org/2000/svg"
        xmlnsXlink="http://www.w3.org/1999/xlink"
        viewBox={{`0 0 ${{width}} ${{height}}`}}
      >
        <g>
          {/* 绘制可视化内容 */}
          <circle
            cx={{width / 2}}
            cy={{height / 2}}
            r={{Math.min(width, height) / 4}}
            fill={{options.color}}
            stroke={{theme.colors.border.medium}}
            strokeWidth={{2}}
          />
          <text
            x={{width / 2}}
            y={{height / 2}}
            textAnchor="middle"
            dominantBaseline="middle"
            fontSize={{options.fontSize}}
            fill={{theme.colors.text.primary}}
          >
            {{lastValue.toFixed(2)}}
          </text>
        </g>
      </svg>
      
      {{options.showSeriesCount && (
        <div className={{styles.textBox}}>
          <span>Series count: {{data.series.length}}</span>
        </div>
      )}}
    </div>
  );
}};
"""
    
    def create_panel_editor(self) -> str:
        """创建面板编辑器"""
        return f"""
import React from 'react';
import {{ PanelEditorProps }} from '@grafana/data';
import {{ SimpleOptions }} from '../types';
import {{ Field, Input, Switch, ColorPicker, Slider }} from '@grafana/ui';

export const SimpleEditor: React.FC<PanelEditorProps<SimpleOptions>> = ({{ options, onOptionsChange }}) => {{
  const onColorChange = (color: string) => {{
    onOptionsChange({{
      ...options,
      color,
    }});
  }};

  const onFontSizeChange = (fontSize: number) => {{
    onOptionsChange({{
      ...options,
      fontSize,
    }});
  }};

  const onShowSeriesCountChange = (showSeriesCount: boolean) => {{
    onOptionsChange({{
      ...options,
      showSeriesCount,
    }});
  }};

  const onTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {{
    onOptionsChange({{
      ...options,
      text: event.target.value,
    }});
  }};

  return (
    <>
      <Field label="Display text">
        <Input
          width={{40}}
          value={{options.text}}
          onChange={{onTextChange}}
          placeholder="Enter display text"
        />
      </Field>
      
      <Field label="Color">
        <ColorPicker
          color={{options.color}}
          onChange={{onColorChange}}
        />
      </Field>
      
      <Field label="Font size">
        <Slider
          min={{10}}
          max={{100}}
          value={{options.fontSize}}
          onChange={{onFontSizeChange}}
        />
      </Field>
      
      <Field label="Show series count">
        <Switch
          value={{options.showSeriesCount}}
          onChange={{onShowSeriesCountChange}}
        />
      </Field>
    </>
  );
}};
"""
    
    def create_panel_types(self) -> str:
        """创建面板类型定义"""
        return f"""
export interface SimpleOptions {{
  text: string;
  color: string;
  fontSize: number;
  showSeriesCount: boolean;
}}

export const defaults: SimpleOptions = {{
  text: 'Default text',
  color: 'green',
  fontSize: 16,
  showSeriesCount: false,
}};

export interface FieldDisplayOptions {{
  displayName?: string;
  unit?: string;
  decimals?: number;
  min?: number;
  max?: number;
  color?: {{
    mode: string;
    value?: string;
  }};
}}

export interface PanelData {{
  values: number[];
  timestamps: number[];
  labels?: string[];
}}
"""
    
    def create_panel_module(self) -> str:
        """创建面板模块"""
        return f"""
import {{ PanelPlugin }} from '@grafana/data';
import {{ SimpleOptions, defaults }} from './types';
import {{ SimplePanel }} from './SimplePanel';
import {{ SimpleEditor }} from './SimpleEditor';

export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel)
  .setPanelOptions((builder) => {{
    return builder
      .addTextInput({{
        path: 'text',
        name: 'Display text',
        description: 'Text to display in the panel',
        defaultValue: defaults.text,
      }})
      .addColorPicker({{
        path: 'color',
        name: 'Color',
        description: 'Color of the visualization',
        defaultValue: defaults.color,
      }})
      .addSliderInput({{
        path: 'fontSize',
        name: 'Font size',
        description: 'Font size for the text',
        defaultValue: defaults.fontSize,
        settings: {{
          min: 10,
          max: 100,
          step: 1,
        }},
      }})
      .addBooleanSwitch({{
        path: 'showSeriesCount',
        name: 'Show series count',
        description: 'Show the number of series in the panel',
        defaultValue: defaults.showSeriesCount,
      }});
  }})
  .setPanelChangeHandler((panel, prevPluginId, prevOptions) => {{
    // 处理面板迁移逻辑
    if (prevPluginId === 'old-panel-id') {{
      return {{
        ...panel,
        options: {{
          ...defaults,
          text: prevOptions.oldText || defaults.text,
        }},
      }};
    }}
    return panel;
  }})
  .setNoPadding();
"""
    
    def create_advanced_panel_features(self) -> str:
        """创建高级面板功能"""
        return f"""
import React, {{ useMemo }} from 'react';
import {{
  PanelProps,
  Field,
  FieldType,
  getFieldDisplayName,
  formattedValueToString,
  getDisplayProcessor,
}} from '@grafana/data';
import {{ useTheme2 }} from '@grafana/ui';
import {{ AdvancedOptions }} from '../types';

interface Props extends PanelProps<AdvancedOptions> {{}}

export const AdvancedPanel: React.FC<Props> = ({{ options, data, width, height, fieldConfig, timeZone }}) => {{
  const theme = useTheme2();
  
  // 处理字段配置
  const processedData = useMemo(() => {{
    if (!data.series.length) {{
      return [];
    }}
    
    return data.series.map((series) => {{
      return series.fields.map((field) => {{
        const displayName = getFieldDisplayName(field, series, data.series);
        const processor = getDisplayProcessor({{
          field,
          theme,
          timeZone,
        }});
        
        return {{
          name: displayName,
          values: field.values.toArray().map((value) => {{
            const processed = processor(value);
            return {{
              raw: value,
              formatted: formattedValueToString(processed),
              color: processed.color,
            }};
          }}),
          config: field.config,
        }};
      }});
    }}).flat();
  }}, [data, theme, timeZone]);
  
  // 数据转换
  const transformedData = useMemo(() => {{
    if (!processedData.length) return [];
    
    switch (options.transformType) {{
      case 'aggregate':
        return processedData.map((field) => {{
          const values = field.values.map(v => v.raw).filter(v => typeof v === 'number');
          const sum = values.reduce((a, b) => a + b, 0);
          const avg = sum / values.length;
          const min = Math.min(...values);
          const max = Math.max(...values);
          
          return {{
            ...field,
            aggregated: {{ sum, avg, min, max, count: values.length }},
          }};
        }});
      
      case 'filter':
        return processedData.map((field) => {{
          const filtered = field.values.filter((value) => {{
            const numValue = typeof value.raw === 'number' ? value.raw : 0;
            return numValue >= (options.filterMin || 0) && numValue <= (options.filterMax || 100);
          }});
          
          return {{ ...field, values: filtered }};
        }});
      
      default:
        return processedData;
    }}
  }}, [processedData, options]);
  
  // 渲染逻辑
  const renderVisualization = () => {{
    switch (options.visualizationType) {{
      case 'bar':
        return renderBarChart();
      case 'line':
        return renderLineChart();
      case 'scatter':
        return renderScatterPlot();
      default:
        return renderTable();
    }}
  }};
  
  const renderBarChart = () => {{
    const barWidth = width / Math.max(transformedData.length, 1);
    const maxValue = Math.max(...transformedData.flatMap(f => f.values.map(v => v.raw as number)));
    
    return (
      <svg width={{width}} height={{height}}>
        {{transformedData.map((field, index) => {{
          const value = field.values[field.values.length - 1]?.raw as number || 0;
          const barHeight = (value / maxValue) * (height - 40);
          
          return (
            <g key={{index}}>
              <rect
                x={{index * barWidth + 10}}
                y={{height - barHeight - 20}}
                width={{barWidth - 20}}
                height={{barHeight}}
                fill={{field.values[field.values.length - 1]?.color || theme.colors.primary.main}}
                stroke={{theme.colors.border.medium}}
              />
              <text
                x={{index * barWidth + barWidth / 2}}
                y={{height - 5}}
                textAnchor="middle"
                fontSize={{12}}
                fill={{theme.colors.text.primary}}
              >
                {{field.name}}
              </text>
            </g>
          );
        }})}}
      </svg>
    );
  }};
  
  const renderLineChart = () => {{
    if (!transformedData.length) return null;
    
    const points = transformedData[0].values.map((value, index) => {{
      const x = (index / (transformedData[0].values.length - 1)) * (width - 40) + 20;
      const y = height - ((value.raw as number) / 100) * (height - 40) - 20;
      return `${{x}},${{y}}`;
    }}).join(' ');
    
    return (
      <svg width={{width}} height={{height}}>
        <polyline
          points={{points}}
          fill="none"
          stroke={{theme.colors.primary.main}}
          strokeWidth={{2}}
        />
        {{transformedData[0].values.map((value, index) => {{
          const x = (index / (transformedData[0].values.length - 1)) * (width - 40) + 20;
          const y = height - ((value.raw as number) / 100) * (height - 40) - 20;
          
          return (
            <circle
              key={{index}}
              cx={{x}}
              cy={{y}}
              r={{3}}
              fill={{value.color || theme.colors.primary.main}}
            />
          );
        }})}}
      </svg>
    );
  }};
  
  const renderScatterPlot = () => {{
    if (transformedData.length < 2) return null;
    
    const xField = transformedData[0];
    const yField = transformedData[1];
    const minLength = Math.min(xField.values.length, yField.values.length);
    
    return (
      <svg width={{width}} height={{height}}>
        {{Array.from({{ length: minLength }}).map((_, index) => {{
          const x = ((xField.values[index].raw as number) / 100) * (width - 40) + 20;
          const y = height - ((yField.values[index].raw as number) / 100) * (height - 40) - 20;
          
          return (
            <circle
              key={{index}}
              cx={{x}}
              cy={{y}}
              r={{5}}
              fill={{yField.values[index].color || theme.colors.primary.main}}
              opacity={{0.7}}
            />
          );
        }})}}
      </svg>
    );
  }};
  
  const renderTable = () => {{
    return (
      <div style={{{{ width, height, overflow: 'auto' }}}}>
        <table style={{{{ width: '100%', borderCollapse: 'collapse' }}}}>
          <thead>
            <tr>
              {{transformedData.map((field, index) => (
                <th
                  key={{index}}
                  style={{{{
                    padding: '8px',
                    borderBottom: `1px solid ${{theme.colors.border.medium}}`,
                    color: theme.colors.text.primary,
                  }}}}
                >
                  {{field.name}}
                </th>
              ))}}
            </tr>
          </thead>
          <tbody>
            {{transformedData[0]?.values.map((_, rowIndex) => (
              <tr key={{rowIndex}}>
                {{transformedData.map((field, colIndex) => (
                  <td
                    key={{colIndex}}
                    style={{{{
                      padding: '8px',
                      borderBottom: `1px solid ${{theme.colors.border.weak}}`,
                      color: field.values[rowIndex]?.color || theme.colors.text.primary,
                    }}}}
                  >
                    {{field.values[rowIndex]?.formatted || 'N/A'}}
                  </td>
                ))}}
              </tr>
            ))}}
          </tbody>
        </table>
      </div>
    );
  }};
  
  return (
    <div style={{{{ width, height, position: 'relative' }}}}>
      {{renderVisualization()}}
      
      {{options.showLegend && (
        <div
          style={{{{
            position: 'absolute',
            top: 10,
            right: 10,
            background: theme.colors.background.secondary,
            padding: '8px',
            borderRadius: '4px',
            border: `1px solid ${{theme.colors.border.medium}}`,
          }}}}
        >
          {{transformedData.map((field, index) => (
            <div key={{index}} style={{{{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}}}>
              <div
                style={{{{
                  width: '12px',
                  height: '12px',
                  backgroundColor: field.values[0]?.color || theme.colors.primary.main,
                  marginRight: '8px',
                  borderRadius: '2px',
                }}}}
              />
              <span style={{{{ color: theme.colors.text.primary, fontSize: '12px' }}}}>
                {{field.name}}
              </span>
            </div>
          ))}}
        </div>
      )}}
    </div>
  );
}};
"""

# 使用示例
panel_plugin = PanelPlugin("mycustom-panel", "My Custom Panel")

# 生成面板组件
panel_component = panel_plugin.create_panel_component()
print("面板组件代码已生成")

# 生成面板编辑器
panel_editor = panel_plugin.create_panel_editor()
print("面板编辑器代码已生成")

# 生成面板类型定义
panel_types = panel_plugin.create_panel_types()
print("面板类型定义已生成")

# 生成面板模块
panel_module = panel_plugin.create_panel_module()
print("面板模块代码已生成")

# 生成高级面板功能
advanced_features = panel_plugin.create_advanced_panel_features()
print("高级面板功能代码已生成")

应用插件开发

1. 应用插件架构

class AppPlugin:
    """应用插件基类"""
    
    def __init__(self, plugin_id: str, name: str):
        self.plugin_id = plugin_id
        self.name = name
        self.pages = []
        self.includes = []
        self.config_pages = []
    
    def create_app_component(self) -> str:
        """创建应用主组件"""
        return f"""
import React from 'react';
import {{ AppRootProps }} from '@grafana/data';
import {{ Route, Switch }} from 'react-router-dom';
import {{ prefixRoute }} from 'utils/utils';
import {{ ExamplePage }} from './pages/ExamplePage';
import {{ ConfigPage }} from './pages/ConfigPage';
import {{ DashboardPage }} from './pages/DashboardPage';

export class App extends React.PureComponent<AppRootProps> {{
  render() {{
    const {{ path, onNavChanged, meta }} = this.props;
    
    return (
      <Switch>
        <Route
          path={{prefixRoute(`${{path}}/config`, meta.baseUrl)}}
          component={{() => (
            <ConfigPage
              plugin={{meta}}
              onNavChanged={{onNavChanged}}
            />
          )}}
        />
        <Route
          path={{prefixRoute(`${{path}}/dashboard`, meta.baseUrl)}}
          component={{() => (
            <DashboardPage
              plugin={{meta}}
              onNavChanged={{onNavChanged}}
            />
          )}}
        />
        <Route
          path={{prefixRoute(`${{path}}`, meta.baseUrl)}}
          component={{() => (
            <ExamplePage
              plugin={{meta}}
              onNavChanged={{onNavChanged}}
            />
          )}}
        />
      </Switch>
    );
  }}
}}
"""
    
    def create_config_page(self) -> str:
        """创建配置页面"""
        return f"""
import React, {{ PureComponent }} from 'react';
import {{ AppPluginMeta, PluginConfigPageProps }} from '@grafana/data';
import {{ Button, Field, Input, Switch, Alert, VerticalGroup }} from '@grafana/ui';
import {{ getBackendSrv }} from '@grafana/runtime';

export interface AppSettings {{
  apiUrl?: string;
  apiKey?: string;
  enableFeatureX?: boolean;
  refreshInterval?: number;
}}

interface Props extends PluginConfigPageProps<AppPluginMeta<AppSettings>> {{}}

interface State {{
  settings: AppSettings;
  isLoading: boolean;
  testResult?: {{
    status: 'success' | 'error';
    message: string;
  }};
}}

export class ConfigPage extends PureComponent<Props, State> {{
  constructor(props: Props) {{
    super(props);
    
    this.state = {{
      settings: {{
        apiUrl: props.plugin.jsonData?.apiUrl || '',
        apiKey: props.plugin.secureJsonData?.apiKey || '',
        enableFeatureX: props.plugin.jsonData?.enableFeatureX || false,
        refreshInterval: props.plugin.jsonData?.refreshInterval || 30,
      }},
      isLoading: false,
    }};
  }}

  onApiUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {{
    this.setState({{
      settings: {{
        ...this.state.settings,
        apiUrl: event.target.value,
      }},
    }});
  }};

  onApiKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {{
    this.setState({{
      settings: {{
        ...this.state.settings,
        apiKey: event.target.value,
      }},
    }});
  }};

  onFeatureToggle = (enableFeatureX: boolean) => {{
    this.setState({{
      settings: {{
        ...this.state.settings,
        enableFeatureX,
      }},
    }});
  }};

  onRefreshIntervalChange = (event: React.ChangeEvent<HTMLInputElement>) => {{
    this.setState({{
      settings: {{
        ...this.state.settings,
        refreshInterval: parseInt(event.target.value, 10),
      }},
    }});
  }};

  onSave = async () => {{
    this.setState({{ isLoading: true }});
    
    try {{
      const {{ settings }} = this.state;
      
      await getBackendSrv().post(`/api/plugins/${{this.props.plugin.id}}/settings`, {{
        enabled: this.props.plugin.enabled,
        pinned: this.props.plugin.pinned,
        jsonData: {{
          apiUrl: settings.apiUrl,
          enableFeatureX: settings.enableFeatureX,
          refreshInterval: settings.refreshInterval,
        }},
        secureJsonData: {{
          apiKey: settings.apiKey,
        }},
      }});
      
      // 重新加载页面以应用新设置
      window.location.reload();
    }} catch (error) {{
      console.error('Failed to save settings:', error);
    }} finally {{
      this.setState({{ isLoading: false }});
    }}
  }};

  onTest = async () => {{
    this.setState({{ isLoading: true, testResult: undefined }});
    
    try {{
      const {{ settings }} = this.state;
      
      const response = await fetch(`${{settings.apiUrl}}/health`, {{
        headers: {{
          'Authorization': `Bearer ${{settings.apiKey}}`,
        }},
      }});
      
      if (response.ok) {{
        this.setState({{
          testResult: {{
            status: 'success',
            message: 'Connection successful!',
          }},
        }});
      }} else {{
        throw new Error(`HTTP ${{response.status}}: ${{response.statusText}}`);
      }}
    }} catch (error) {{
      this.setState({{
        testResult: {{
          status: 'error',
          message: `Connection failed: ${{error.message}}`,
        }},
      }});
    }} finally {{
      this.setState({{ isLoading: false }});
    }}
  }};

  render() {{
    const {{ settings, isLoading, testResult }} = this.state;
    
    return (
      <div>
        <h2>App Configuration</h2>
        
        <VerticalGroup spacing="lg">
          <Field label="API URL" description="The base URL for the external API">
            <Input
              width={{40}}
              value={{settings.apiUrl}}
              onChange={{this.onApiUrlChange}}
              placeholder="https://api.example.com"
            />
          </Field>
          
          <Field label="API Key" description="Authentication key for the API">
            <Input
              width={{40}}
              type="password"
              value={{settings.apiKey}}
              onChange={{this.onApiKeyChange}}
              placeholder="Enter API key"
            />
          </Field>
          
          <Field label="Enable Feature X" description="Enable experimental feature X">
            <Switch
              value={{settings.enableFeatureX}}
              onChange={{this.onFeatureToggle}}
            />
          </Field>
          
          <Field label="Refresh Interval (seconds)" description="How often to refresh data">
            <Input
              width={{20}}
              type="number"
              value={{settings.refreshInterval}}
              onChange={{this.onRefreshIntervalChange}}
              min={{5}}
              max={{3600}}
            />
          </Field>
          
          {{testResult && (
            <Alert
              title={{testResult.status === 'success' ? 'Test Successful' : 'Test Failed'}}
              severity={{testResult.status === 'success' ? 'success' : 'error'}}
            >
              {{testResult.message}}
            </Alert>
          )}}
          
          <div>
            <Button
              variant="secondary"
              onClick={{this.onTest}}
              disabled={{isLoading || !settings.apiUrl || !settings.apiKey}}
            >
              Test Connection
            </Button>
            <Button
              variant="primary"
              onClick={{this.onSave}}
              disabled={{isLoading}}
              style={{{{ marginLeft: '8px' }}}}
            >
              Save
            </Button>
          </div>
        </VerticalGroup>
      </div>
    );
  }}
}}
"""
    
    def create_example_page(self) -> str:
        """创建示例页面"""
        return f"""
import React, {{ PureComponent }} from 'react';
import {{ AppPluginMeta, NavModel }} from '@grafana/data';
import {{ Page, Alert, Button, Card, VerticalGroup, HorizontalGroup }} from '@grafana/ui';
import {{ getBackendSrv }} from '@grafana/runtime';
import {{ AppSettings }} from './ConfigPage';

interface Props {{
  plugin: AppPluginMeta<AppSettings>;
  onNavChanged: (nav: NavModel) => void;
}}

interface State {{
  data: any[];
  isLoading: boolean;
  error?: string;
}}

export class ExamplePage extends PureComponent<Props, State> {{
  constructor(props: Props) {{
    super(props);
    
    this.state = {{
      data: [],
      isLoading: false,
    }};
  }}

  componentDidMount() {{
    this.props.onNavChanged({{
      text: 'Example App',
      subTitle: 'Grafana plugin example application',
      url: '',
      children: [
        {{
          text: 'Home',
          url: '',
          active: true,
        }},
        {{
          text: 'Dashboard',
          url: '/dashboard',
        }},
        {{
          text: 'Configuration',
          url: '/config',
        }},
      ],
    }});
    
    this.loadData();
  }}

  loadData = async () => {{
    this.setState({{ isLoading: true, error: undefined }});
    
    try {{
      const {{ plugin }} = this.props;
      const apiUrl = plugin.jsonData?.apiUrl;
      const apiKey = plugin.secureJsonData?.apiKey;
      
      if (!apiUrl || !apiKey) {{
        throw new Error('API URL and API Key must be configured');
      }}
      
      const response = await fetch(`${{apiUrl}}/data`, {{
        headers: {{
          'Authorization': `Bearer ${{apiKey}}`,
          'Content-Type': 'application/json',
        }},
      }});
      
      if (!response.ok) {{
        throw new Error(`HTTP ${{response.status}}: ${{response.statusText}}`);
      }}
      
      const data = await response.json();
      this.setState({{ data }});
    }} catch (error) {{
      this.setState({{ error: error.message }});
    }} finally {{
      this.setState({{ isLoading: false }});
    }}
  }};

  render() {{
    const {{ data, isLoading, error }} = this.state;
    const {{ plugin }} = this.props;
    
    return (
      <Page navModel={{{{ text: 'Example App' }}}}>
        <Page.Contents>
          <VerticalGroup spacing="lg">
            <div>
              <h1>Welcome to {{plugin.name}}</h1>
              <p>This is an example Grafana app plugin demonstrating various features.</p>
            </div>
            
            {{error && (
              <Alert title="Error" severity="error">
                {{error}}
                <br />
                <Button variant="secondary" size="sm" onClick={{this.loadData}}>
                  Retry
                </Button>
              </Alert>
            )}}
            
            <Card>
              <Card.Heading>Data Overview</Card.Heading>
              <Card.Actions>
                <Button
                  variant="secondary"
                  onClick={{this.loadData}}
                  disabled={{isLoading}}
                >
                  Refresh
                </Button>
              </Card.Actions>
              
              {{isLoading ? (
                <div>Loading...</div>
              ) : (
                <div>
                  <p>Total records: {{data.length}}</p>
                  {{data.length > 0 && (
                    <div>
                      <h4>Sample Data:</h4>
                      <pre style={{{{ background: '#f5f5f5', padding: '10px', borderRadius: '4px' }}}}>
                        {{JSON.stringify(data.slice(0, 3), null, 2)}}
                      </pre>
                    </div>
                  )}}
                </div>
              )}}
            </Card>
            
            <Card>
              <Card.Heading>Quick Actions</Card.Heading>
              <HorizontalGroup spacing="md">
                <Button variant="primary" onClick={{() => window.open('/dashboard/new', '_blank')}}>
                  Create Dashboard
                </Button>
                <Button variant="secondary" onClick={{() => window.open('/datasources/new', '_blank')}}>
                  Add Data Source
                </Button>
                <Button variant="secondary" onClick={{() => window.open('/alerting', '_blank')}}>
                  Manage Alerts
                </Button>
              </HorizontalGroup>
            </Card>
            
            <Card>
              <Card.Heading>Plugin Information</Card.Heading>
              <div>
                <p><strong>Version:</strong> {{plugin.info.version}}</p>
                <p><strong>Author:</strong> {{plugin.info.author.name}}</p>
                <p><strong>Description:</strong> {{plugin.info.description}}</p>
                <p><strong>Updated:</strong> {{plugin.info.updated}}</p>
              </div>
            </Card>
          </VerticalGroup>
        </Page.Contents>
      </Page>
    );
  }}
}}
"""
    
    def create_app_module(self) -> str:
        """创建应用模块"""
        return f"""
import {{ AppPlugin }} from '@grafana/data';
import {{ App }} from './components/App';
import {{ ConfigPage }} from './components/ConfigPage';
import {{ AppSettings }} from './components/ConfigPage';

export const plugin = new AppPlugin<AppSettings>()
  .setRootPage(App)
  .addConfigPage({{
    title: 'Configuration',
    icon: 'cog',
    body: ConfigPage,
    id: 'configuration',
  }})
  .configureExtensionLink({{
    title: 'Example App',
    description: 'Open the example application',
    extensionPointId: 'grafana/dashboard/panel/menu',
    configure: () => {{
      return {{
        title: 'Open in Example App',
        description: 'View this data in the example application',
        path: '/a/example-app',
      }};
    }},
  }});
"""
    
    def create_app_types(self) -> str:
        """创建应用类型定义"""
        return f"""
import {{ DataSourceJsonData }} from '@grafana/data';

export interface AppSettings extends DataSourceJsonData {{
  apiUrl?: string;
  enableFeatureX?: boolean;
  refreshInterval?: number;
}}

export interface AppSecureSettings {{
  apiKey?: string;
}}

export interface AppState {{
  isConfigured: boolean;
  lastSync?: string;
  errorCount: number;
}}

export interface NavigationItem {{
  text: string;
  url: string;
  icon?: string;
  active?: boolean;
  children?: NavigationItem[];
}}

export interface PageProps {{
  plugin: any;
  onNavChanged: (nav: any) => void;
}}
"""

# 使用示例
app_plugin = AppPlugin("example-app", "Example App")

# 生成应用组件
app_component = app_plugin.create_app_component()
print("应用组件代码已生成")

# 生成配置页面
config_page = app_plugin.create_config_page()
print("配置页面代码已生成")

# 生成示例页面
example_page = app_plugin.create_example_page()
print("示例页面代码已生成")

# 生成应用模块
app_module = app_plugin.create_app_module()
print("应用模块代码已生成")

# 生成应用类型定义
app_types = app_plugin.create_app_types()
print("应用类型定义已生成")

插件发布与分发

1. 插件签名与发布

class PluginPublisher:
    """插件发布管理器"""
    
    def __init__(self):
        self.signing_config = {}
        self.distribution_channels = []
        self.release_process = []
    
    def create_signing_workflow(self) -> Dict:
        """创建签名工作流"""
        return {
            "github_actions": """
name: Sign and Release Plugin

on:
  push:
    tags:
      - 'v*'

jobs:
  sign-and-release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build plugin
        run: npm run build
        
      - name: Test plugin
        run: npm run test:ci
        
      - name: Sign plugin
        run: npx --yes @grafana/sign-plugin@latest
        env:
          GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }}
          
      - name: Package plugin
        run: |
          mv dist ${{ github.event.repository.name }}
          zip -r ${{ github.event.repository.name }}-${{ github.ref_name }}.zip ${{ github.event.repository.name }}
          
      - name: Create Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false
          
      - name: Upload Release Asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./${{ github.event.repository.name }}-${{ github.ref_name }}.zip
          asset_name: ${{ github.event.repository.name }}-${{ github.ref_name }}.zip
          asset_content_type: application/zip
""",
            "manual_signing": """
# 手动签名步骤

1. 构建插件
npm run build

2. 测试插件
npm run test:ci

3. 签名插件
npx @grafana/sign-plugin@latest

4. 验证签名
npx @grafana/sign-plugin@latest --verify

5. 打包插件
zip -r my-plugin.zip dist/
""",
            "docker_signing": """
# Dockerfile for plugin signing
FROM node:16-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build
RUN npm run test:ci
RUN npx @grafana/sign-plugin@latest

CMD ["npm", "run", "package"]
"""
        }
    
    def create_distribution_config(self) -> Dict:
        """创建分发配置"""
        return {
            "grafana_catalog": {
                "description": "官方Grafana插件目录",
                "requirements": [
                    "插件必须经过签名",
                    "通过安全审查",
                    "符合质量标准",
                    "提供完整文档"
                ],
                "submission_process": [
                    "在GitHub上创建插件仓库",
                    "添加plugin.json配置文件",
                    "提交到Grafana插件目录",
                    "等待审查和批准"
                ]
            },
            "private_registry": {
                "description": "私有插件注册表",
                "setup_steps": [
                    "配置私有npm注册表",
                    "设置访问权限",
                    "配置Grafana插件目录",
                    "部署插件包"
                ],
                "nginx_config": """
server {
    listen 80;
    server_name plugins.company.com;
    
    location /plugins/ {
        alias /var/www/plugins/;
        autoindex on;
        autoindex_format json;
        
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type";
    }
    
    location /api/plugins {
        proxy_pass http://plugin-registry:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
"""
            },
            "docker_registry": {
                "description": "Docker容器分发",
                "dockerfile": """
FROM grafana/grafana:latest

USER root
COPY dist/ /var/lib/grafana/plugins/my-plugin/
RUN chown -R grafana:grafana /var/lib/grafana/plugins/

USER grafana
ENV GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=my-plugin
""",
                "docker_compose": """
version: '3.8'
services:
  grafana:
    build: .
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=my-plugin
    volumes:
      - grafana-data:/var/lib/grafana
      
volumes:
  grafana-data:
"""
            }
        }
    
    def create_release_checklist(self) -> List[str]:
        """创建发布检查清单"""
        return [
            "✅ 代码审查完成",
            "✅ 所有测试通过",
            "✅ 文档更新完成",
            "✅ 版本号更新",
            "✅ CHANGELOG.md更新",
            "✅ 插件签名完成",
            "✅ 安全扫描通过",
            "✅ 性能测试通过",
            "✅ 兼容性测试完成",
            "✅ 用户验收测试通过",
            "✅ 发布说明准备",
            "✅ 回滚计划制定",
            "✅ 监控告警配置",
            "✅ 用户通知发送"
        ]
    
    def generate_plugin_metadata(self, plugin_info: PluginInfo) -> Dict:
        """生成插件元数据"""
        return {
            "id": plugin_info.id,
            "name": plugin_info.name,
            "type": plugin_info.type.value,
            "version": plugin_info.version,
            "description": plugin_info.description,
            "author": plugin_info.author,
            "keywords": plugin_info.keywords,
            "repository": {
                "type": "git",
                "url": f"https://github.com/your-org/{plugin_info.id}"
            },
            "bugs": {
                "url": f"https://github.com/your-org/{plugin_info.id}/issues"
            },
            "homepage": f"https://github.com/your-org/{plugin_info.id}#readme",
            "license": "Apache-2.0",
            "grafana": {
                "dependency": ">=8.0.0",
                "type": plugin_info.type.value,
                "id": plugin_info.id
            },
            "dist": {
                "tarball": f"https://github.com/your-org/{plugin_info.id}/releases/download/v{plugin_info.version}/{plugin_info.id}-{plugin_info.version}.zip"
            },
            "engines": {
                "grafana": ">=8.0.0",
                "node": ">=16.0.0"
            }
        }
    
    def create_update_mechanism(self) -> Dict:
        """创建更新机制"""
        return {
            "auto_update_check": """
// 自动更新检查
const checkForUpdates = async () => {
  try {
    const response = await fetch('/api/plugins/my-plugin/updates');
    const updateInfo = await response.json();
    
    if (updateInfo.hasUpdate) {
      showUpdateNotification(updateInfo);
    }
  } catch (error) {
    console.error('Failed to check for updates:', error);
  }
};

// 每24小时检查一次更新
setInterval(checkForUpdates, 24 * 60 * 60 * 1000);
""",
            "migration_script": """
// 数据迁移脚本
const migratePluginData = (oldVersion, newVersion) => {
  const migrations = {
    '1.0.0': (data) => {
      // 从0.x版本迁移到1.0.0
      return {
        ...data,
        newField: 'defaultValue'
      };
    },
    '2.0.0': (data) => {
      // 从1.x版本迁移到2.0.0
      return {
        ...data,
        settings: {
          ...data.config,
          newSetting: true
        }
      };
    }
  };
  
  let currentData = data;
  const versionsToMigrate = getVersionsBetween(oldVersion, newVersion);
  
  for (const version of versionsToMigrate) {
    if (migrations[version]) {
      currentData = migrations[version](currentData);
    }
  }
  
  return currentData;
};
""",
            "rollback_strategy": """
// 回滚策略
const rollbackPlugin = async (targetVersion) => {
  try {
    // 1. 备份当前配置
    const currentConfig = await getPluginConfig();
    await backupConfig(currentConfig);
    
    // 2. 下载目标版本
    const targetPackage = await downloadPluginVersion(targetVersion);
    
    // 3. 停止当前插件
    await stopPlugin();
    
    // 4. 安装目标版本
    await installPlugin(targetPackage);
    
    // 5. 迁移配置(如果需要)
    const migratedConfig = await migrateConfigDown(currentConfig, targetVersion);
    await setPluginConfig(migratedConfig);
    
    // 6. 启动插件
    await startPlugin();
    
    // 7. 验证回滚
    const health = await checkPluginHealth();
    if (!health.ok) {
      throw new Error('Plugin health check failed after rollback');
    }
    
    return { success: true, version: targetVersion };
  } catch (error) {
    console.error('Rollback failed:', error);
    return { success: false, error: error.message };
  }
};
"""
        }

# 使用示例
publisher = PluginPublisher()

# 创建签名工作流
signing_workflow = publisher.create_signing_workflow()
print("签名工作流已创建")

# 创建分发配置
distribution_config = publisher.create_distribution_config()
print("分发配置已创建")

# 生成发布检查清单
release_checklist = publisher.create_release_checklist()
print(f"发布检查清单包含 {len(release_checklist)} 项")

# 生成插件元数据
plugin_info = PluginInfo(
    id="example-plugin",
    name="Example Plugin",
    type=PluginType.PANEL,
    version="1.0.0",
    description="An example Grafana plugin",
    author="Your Name",
    keywords=["panel", "visualization"],
    screenshots=[],
    logos={"small": "img/logo.svg", "large": "img/logo.svg"},
    links=[],
    dependencies={"grafanaDependency": ">=8.0.0"},
    state=PluginState.STABLE,
    signature=PluginSignatureStatus.VALID,
    updated=datetime.now().strftime("%Y-%m-%d")
)

metadata = publisher.generate_plugin_metadata(plugin_info)
print("插件元数据已生成")

# 创建更新机制
update_mechanism = publisher.create_update_mechanism()
print("更新机制已创建")

总结

关键要点

  1. 插件类型理解

    • 数据源插件:连接外部数据源
    • 面板插件:创建自定义可视化
    • 应用插件:提供完整应用功能
    • 渲染器插件:处理图像渲染
  2. 开发环境搭建

    • 使用官方脚手架工具
    • 配置TypeScript和Webpack
    • 设置测试环境
    • 配置开发服务器
  3. 插件架构设计

    • 模块化组件结构
    • 类型安全的TypeScript
    • 响应式UI设计
    • 性能优化考虑

最佳实践

  1. 代码质量

    • 遵循TypeScript最佳实践
    • 编写全面的单元测试
    • 使用ESLint和Prettier
    • 实施代码审查流程
  2. 用户体验

    • 提供直观的配置界面
    • 实现响应式设计
    • 添加加载状态和错误处理
    • 支持主题切换
  3. 性能优化

    • 实现数据缓存机制
    • 优化渲染性能
    • 减少不必要的重新渲染
    • 使用虚拟化技术
  4. 安全考虑

    • 验证用户输入
    • 安全存储敏感信息
    • 实施访问控制
    • 定期安全审计

下一步学习建议

  1. 深入学习Grafana API

    • 研究Grafana数据模型
    • 掌握插件生命周期
    • 学习高级配置选项
  2. 扩展功能开发

    • 集成第三方库
    • 开发复杂可视化
    • 实现实时数据流
  3. 社区参与

    • 贡献开源插件
    • 参与Grafana社区讨论
    • 分享开发经验
  4. 持续改进

    • 收集用户反馈
    • 监控插件性能
    • 定期更新依赖
    • 跟进Grafana新特性

通过本教程的学习,你应该能够: - 理解Grafana插件开发的核心概念 - 搭建完整的插件开发环境 - 开发各种类型的Grafana插件 - 实施插件发布和维护流程 - 遵循插件开发的最佳实践

继续实践和探索,你将能够创建出功能强大、用户友好的Grafana插件!