diff --git a/packages/create-plugin/templates/common/CLAUDE.md b/packages/create-plugin/templates/common/CLAUDE.md new file mode 100644 index 0000000000..eef4bd20cf --- /dev/null +++ b/packages/create-plugin/templates/common/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/packages/create-plugin/templates/datasource/.config/AGENTS/instructions.md b/packages/create-plugin/templates/datasource/.config/AGENTS/instructions.md new file mode 100644 index 0000000000..3cdf555834 --- /dev/null +++ b/packages/create-plugin/templates/datasource/.config/AGENTS/instructions.md @@ -0,0 +1,105 @@ +You are an expert Grafana datasource plugin developer for this project. + +## Your role + +- You are fluent in TypeScript and React (frontend) +- You are fluent in Go (backend) +- You know how to use Grafana dashboards +- You know how to setup and manage Grafana datasources + +## Project knowledge + +This repository contains a **Grafana datasource**, providing a custom datasource for Grafana. +Datasource plugins are used to fetch and query data from external systems. + +### Plugin anatomy + +A typical datasource with backend plugin includes: + +**plugin.json** + +- Declares plugin ID, type (`datasource`), name, version +- Loaded by Grafana at startup + +**Main module (`src/module.ts`)** + +- Exports: `new DataSourcePlugin(DataSource)` +- Registers query editor, config editor + +**Data source (`src/datasource.ts`)** + +- Frontend datasource that extends DataSourceWithBackend. +- Connects the UI to the backend, provides the default query, applies template variables, filters queries, and sends them to the Go backend for execution + +**Query editor (`src/QueryEditor.tsx`)** + +- React component where users build and customize queries that will be sent to the data source + +**Config editor (`src/ConfigEditor.tsx`)** + +- React component where users manage and configure a data source instance +- Configures instance specific settings (like URLs or credentials) + +**Main module (`pkg/main.go`)** + +- Register a factory function with `grafana-plugin-sdk-go` to create datasource backend instances + +**Data source (`pkg/plugin/datasource.go`)** + +- Backend datasource that Implements QueryData (receives queries from frontend, unmarshals into queryModel, returns data frames) +- CheckHealth (validates API key from settings) +- Dispose (cleanup hook). +- NewDatasource factory called when Grafana starts instance of plugin + +### Repository layout + +- `src/` - Frontend (TypeScript/React) +- `src/plugin.json` — Plugin manifest (metadata) +- `src/module.ts` — Frontend entry point +- `src/datasource.ts` - Datasource implementation +- `src/components/QueryEditor.tsx` — Query builder UI +- `src/components/ConfigEditor.tsx` — Data source settings UI +- `src/types.ts` — Shared frontend models +- `tests/` — E2E tests (if present) +- `provisioning/` — Local development provisioning +- `README.md` — Human documentation +- `pkg/` - Backend (Go) +- `pkg/main.go` - Backend entry point +- `pkg/plugin/datasource.go` - Datasource implementation +- `Magefile.go` - Backend build tasks +- `package.json` - Frontend build scripts + deps + +## Coding guidelines + +- Use **TypeScript** (in strict mode), functional React components, and idiomatic patterns +- Use **@grafana/ui**, **@grafana/data**, **@grafana/runtime** +- Use **`useTheme2()`** for all colors, spacing, typography +- **Never hardcode** colors, spacing, padding, or font sizes +- Use **Emotion** + `useStyles2()` + theme tokens for styling +- Keep layouts responsive (use `width`/`height`) +- Avoid new dependencies unless necessary + Grafana-compatible +- Maintain consistent file structure and predictable types +- Use **`@grafana/plugin-e2e`** for E2E tests and **always use versioned selectors** to interact with the Grafana UI. + +## Boundaries + +You must **NOT**: + +- Change plugin ID or plugin type in `plugin.json` +- Modify anything inside `.config/*` +- Remove/change existing query model without a migration handler +- Break public APIs (query model) +- Use the local file system +- Use environment variables +- Execute arbitrary code in the backend + +You **SHOULD**: + +- Maintain backward compatibility +- Preserve query model schema unless migration handler is added +- Follow official Grafana datasource plugin patterns +- Use idiomatic React + TypeScript +- Use secureJsonData instead of jsonData for credentials and sensitive data +- Error happening should be logged with level `error` + +## Instructions for specific tasks diff --git a/packages/create-plugin/templates/datasource/.config/AGENTS/tasks/support-template-variables.md b/packages/create-plugin/templates/datasource/.config/AGENTS/tasks/support-template-variables.md new file mode 100644 index 0000000000..1502424e86 --- /dev/null +++ b/packages/create-plugin/templates/datasource/.config/AGENTS/tasks/support-template-variables.md @@ -0,0 +1,307 @@ +# Add Support for Template & Query Variables + +Variables are placeholders you can use to create templated queries and dynamic dashboards. In a data source, this means: + +- Using template variables inside query fields +- Implementing `metricFindQuery` and a `VariableQueryEditor` so the data source can be used as a **Query** type variable. + +Always complete **all three sections**: + +1. Interpolate template variables in queries +2. Support query variables via `metricFindQuery` +3. Add a `VariableQueryEditor` + +## 1. Interpolate Template Variables in Queries + +File: `src/datasource.ts` (or your data source class file) + +### 1.1 Import `getTemplateSrv` + +```ts +import { getTemplateSrv } from '@grafana/runtime'; +``` + +- `getTemplateSrv()` returns `TemplateSrv`, which exposes `replace()` for variable interpolation. + +### 1.2 Decide which fields support variables + +In your query model (e.g. `MyQuery`), identify all string fields that should accept `$var`: + +```ts +export interface MyQuery { + // existing… + rawQuery?: string; // e.g. SQL / text query + namespace?: string; // optional selector + // other fields… +} +``` + +Rules: + +- Only enable variables where they actually make sense (query strings, selectors, filters). +- Document which fields support variables if you have query editor help. + +### 1.3 Apply `replace()` in `query()` + +Inside your `DataSource` implementation: + +```ts +import { DataQueryRequest, DataQueryResponse } from '@grafana/schema'; + +export class DataSource extends DataSourceApi { + constructor( + instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { + super(instanceSettings); + } + + async query(options: DataQueryRequest): Promise { + const targets = options.targets.filter((t) => !t.hide); + + const interpolatedTargets = targets.map((target) => { + const rawQuery = this.templateSrv.replace( + target.rawQuery ?? '', + options.scopedVars // include scoped vars for panel/time range + ); + + const namespace = this.templateSrv.replace(target.namespace ?? '', options.scopedVars); + + return { + ...target, + rawQuery, + namespace, + }; + }); + + // Use interpolatedTargets when building your backend request. + return this.doRequest(interpolatedTargets, options); + } + + private async doRequest(targets: MyQuery[], options: DataQueryRequest): Promise { + // existing implementation… + } +} +``` + +- `replace(template, scopedVars?)` replaces `$var` with current values and supports built-ins like `$__from` / `$__to`. +- You can keep interpolation localized to **only** the fields that support variables (e.g. `rawQuery`, `namespace`). + +### 1.4 Handle multi-value variables (optional) + +For multi-value variables, choose a format that matches your backend (CSV, JSON array, etc.): + +```ts +const csvQuery = getTemplateSrv().replace( + target.rawQuery ?? '', + options.scopedVars, + 'csv' // built-in format option +); +``` + +Or provide a custom formatter: + +```ts +const formatter = (value: string | string[]): string => { + if (typeof value === 'string') { + return value; + } + + // Example: join with OR + if (value.length > 1) { + return '(' + value.map((v) => `"${v}"`).join(' OR ') + ')'; + } + + return value[0]; +}; + +const formattedQuery = getTemplateSrv().replace(target.rawQuery ?? '', options.scopedVars, formatter); +``` + +Rules: + +- Use a built-in format (`csv`, etc.) where possible. +- Use a custom interpolation function only if built-ins don’t match your protocol. + +## 2. Implement `metricFindQuery` for Query Variables + +File: `src/datasource.ts` + +A **query variable** lets Grafana call your data source to get variable values. To support this, you override `metricFindQuery` in your `DataSourceApi` implementation. + +### 2.1 Define a variable query model + +File: `src/types.ts` + +```ts +export interface MyVariableQuery { + namespace: string; + rawQuery: string; +} +``` + +- Keep this model minimal – only the fields needed to fetch variable values. +- This is separate from your “regular” `MyQuery` if that simplifies things. + +> If a plain text query is enough, you can leave `query` as `string` and skip the model entirely: +> `async metricFindQuery(query: string, options?: any)` + +### 2.2 Implement `metricFindQuery` + +File: `src/datasource.ts` + +```ts +import { MetricFindValue } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import { MyVariableQuery } from './types'; + +export class DataSource extends DataSourceApi { + // existing constructor, query()... + + async metricFindQuery(variableQuery: MyVariableQuery | string, options?: any): Promise { + if (typeof variableQuery === 'string') { + const interpolated = this.templateSrv.replace(variableQuery); + const response = await this.fetchVariableValues({ rawQuery: interpolated }); + return response.map((name) => ({ text: name })); + } + + // If using MyVariableQuery model: + const namespace = this.templateSrv.replace(variableQuery.namespace); + const rawQuery = this.templateSrv.replace(variableQuery.rawQuery); + + const response = await this.fetchMetricNames(namespace, rawQuery); + + // Adapt this to match your backend response + return response.data.map((item: any) => ({ + text: item.name, + // optional: value: item.id, + })); + } + + private async fetchMetricNames(namespace: string, rawQuery: string) { + // call backend/API and return data in a consistent shape + } + + private async fetchVariableValues(args: { rawQuery: string }) { + // simplified variant if using a simple string-based query + } +} +``` + +Rules: + +- Return an array of `{ text: string }` (`MetricFindValue[]`). +- Use `getTemplateSrv().replace` inside `metricFindQuery` so variable queries can themselves use other variables (e.g. cascading variables). +- Keep queries lightweight – `metricFindQuery` can be called often by Grafana. + +## 3. Add a `VariableQueryEditor` + +If you use a structured `MyVariableQuery` model, add a small React editor so users can configure it from the variable UI. + +### 3.1 Create `VariableQueryEditor` + +File: `src/VariableQueryEditor.tsx` + +```tsx +import React, { useState } from 'react'; +import { MyVariableQuery } from './types'; +import { InlineField, InlineFieldRow, LoadingPlaceholder, Select } from '@grafana/ui'; + +interface VariableQueryProps { + query: MyVariableQuery; + onChange: (query: MyVariableQuery, definition: string) => void; +} + +export const VariableQueryEditor = ({ query, onChange }: VariableQueryProps) => { + const [state, setState] = useState(query); + + const saveQuery = () => { + // Second argument is the human-readable label shown in the variable list + const definition = `${state.rawQuery} (${state.namespace})`; + onChange(state, definition); + }; + + const handleChange = (event: React.FormEvent) => { + const { name, value } = event.currentTarget; + + const next = { + ...state, + [name]: value, + }; + + setState(next); + }; + + return ( + <> + + + + + + + + + + + + ); +}; +``` + +### 3.2 Create a `VariableSupport` file for the plugin + +File: `src/variableSupport.ts` + +```ts +export class MyVariableSupport extends CustomVariableSupport { + editor = VariableQueryEditor; + + constructor(private datasource: Datasource) { + super(); + } + + query(request: DataQueryRequest): Observable<{ data: MetricFindValue[] }> { + const [query] = request.targets; + const { range, scopedVars } = request; + + const result = this.datasource.metricFindQuery(query, { scopedVars, range }); + return from(result).pipe(map((data) => ({ data }))); + } +} +``` + +- `editor = VariableQueryEditor;` wires your editor into Grafana’s variable UI. + +### 3.3 Assign `VariableSupport` to `this.variables` in the datasource + +File: `src/datasource.ts` + +```ts +export class DataSource extends DataSourceApi { + constructor( + instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { + super(instanceSettings); + // assign the variable property to the new VariableSupport + // to add variable support for this datasource + this.variables = new VariableSupport(this); + } + // existing functions()… +} +``` diff --git a/packages/create-plugin/templates/datasource/AGENTS.md b/packages/create-plugin/templates/datasource/AGENTS.md new file mode 100644 index 0000000000..9131eeca06 --- /dev/null +++ b/packages/create-plugin/templates/datasource/AGENTS.md @@ -0,0 +1,10 @@ +--- +name: datasource-plugin-agent +description: Develops Grafana datasource plugins +--- + +## Project knowledge + +This repository contains a **Grafana datasource plugin**. Follow the [instructions](./.config/AGENTS/instructions.md) before doing any changes. + +All build, lint, test, and Docker dev-server commands are documented in the "Getting started" section of [README.md](./README.md). Prefer running the non-watch versions of these commands. diff --git a/packages/create-plugin/templates/panel/.config/AGENTS/instructions.md b/packages/create-plugin/templates/panel/.config/AGENTS/instructions.md index 3e07dd0727..a75789bc45 100644 --- a/packages/create-plugin/templates/panel/.config/AGENTS/instructions.md +++ b/packages/create-plugin/templates/panel/.config/AGENTS/instructions.md @@ -1,8 +1,3 @@ ---- -name: panel-plugin-agent-fundamentals -description: Develops Grafana panel plugins ---- - You are an expert Grafana panel plugin developer for this project. ## Your role