Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .config/.cprc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "7.11.0"
}
16 changes: 16 additions & 0 deletions .config/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
*
* In order to extend the configuration follow the steps in .config/README.md
*/

module.exports = {
endOfLine: 'auto',
printWidth: 120,
trailingComma: 'es5',
semi: true,
jsxSingleQuote: false,
singleQuote: true,
useTabs: false,
tabWidth: 2,
};
173 changes: 173 additions & 0 deletions .config/AGENTS/e2e-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
name: e2e testing instructions for a grafana plugin
description: Guides how to write e2e tests using @grafana/plugin-e2e
---

# E2E testing a Grafana plugin

This plugin uses `@grafana/plugin-e2e` and Playwright for end-to-end testing.

- Import `test` and `expect` from `@grafana/plugin-e2e`, or from your project's fixtures entrypoint (for example `./fixtures`) if it re-exports them. Do not import them from `@playwright/test` directly.
- Always use `@grafana/plugin-e2e` fixtures and page models instead of raw Playwright navigation. They handle Grafana version differences automatically.
- Place test files in `tests/` as `*.spec.ts`.
- Each test must be independent and assume fresh state.
- If tests fail against newer Grafana versions, update `@grafana/plugin-e2e` first. It evolves alongside Grafana core to handle selector and API changes.

## Selecting elements

### Grafana selectors

- Use Grafana e2e-selectors whenever possible. Always get them from the `selectors` fixture provided by `@grafana/plugin-e2e` - never import from `@grafana/e2e-selectors` directly. The fixture resolves the correct selectors for the Grafana version under test.
- Always use the `getByGrafanaSelector` method (exposed by all plugin-e2e page models) to resolve selectors to Playwright locators. It handles the `aria-label` vs `data-testid` difference across Grafana versions automatically.
```typescript
panelEditPage.getByGrafanaSelector(selectors.components.CodeEditor.container).click();
```

### Scoping locators

Scope locators to the narrowest context possible.
```typescript
// bad - matches any "URL" text on the page
page.getByText('URL').click();
// good - scoped to the plugin's wrapper
page.getByTestId('plugin-url-wrapper').getByText('URL').click();
```

### Form elements

The `InlineField` and `Field` components can be used interchangeably in the examples below.

**Input** - use `getByRole('textbox', { name: '<label>' })` where the name matches the wrapping `InlineField` label.

```tsx
// component
<InlineField label="Auth key">
<Input value={value} onChange={handleOnChange} id="config-auth-key" />
</InlineField>
```

```typescript
// test
await page.getByRole('textbox', { name: 'Auth key' }).fill('..');
```

**Select** - use `getByRole('combobox', { name: '<label>' })` to open the dropdown. Assert options using `selectors.components.Select.option` via `getByGrafanaSelector`. Note: the `Select` component requires `inputId` (not `id`) for label association.

```tsx
// component
<InlineField label="Auth type">
<Select inputId="config-auth-type" value={value} options={options} onChange={handleOnChange} />
</InlineField>
```

```typescript
// test
await page.getByRole('combobox', { name: 'Auth type' }).click();
const option = selectors.components.Select.option;
await expect(configPage.getByGrafanaSelector(option)).toHaveText(['val1', 'val2']);
```

**Checkbox** - use `getByRole('checkbox', { name: '<label>' })`. The underlying input is not directly clickable so you must pass `{ force: true }`.

```tsx
// component
<InlineField label="TLS Enabled">
<Checkbox id="config-tls-enabled" value={value} onChange={handleOnChange} />
</InlineField>
```

```typescript
// test
await page.getByRole('checkbox', { name: 'TLS Enabled' }).uncheck({ force: true });
await expect(page.getByRole('checkbox', { name: 'TLS Enabled' })).not.toBeChecked();
```

**InlineSwitch** - use `getByRole('switch', { name: /<label>/i })`. Like Checkbox, requires `{ force: true }`. Prefer `getByRole` over `getByLabel` — newer Grafana versions add `aria-label` to the `<label>` element itself, causing `getByLabel` to match multiple elements in strict mode.

```tsx
// component
<InlineField label="TLS Enabled">
<InlineSwitch label="TLS Enabled" value={value} onChange={handleOnChange} />
</InlineField>
```

```typescript
// test
await page.getByRole('switch', { name: /TLS Enabled/i }).uncheck({ force: true });
await expect(page.getByRole('switch', { name: /TLS Enabled/i })).not.toBeChecked();
```

## Using the plugin-e2e API

`@grafana/plugin-e2e` exposes page models and fixtures that encapsulate common UI operations and handle Grafana version differences.

To discover all available fixtures, options, models and matchers, read the exports in `node_modules/@grafana/plugin-e2e/dist/index.d.ts` (or refer to the `@grafana/plugin-e2e` package README/docs).

### Fixtures

Fixtures follow a naming convention that indicates how the resource is obtained:

- **camelCase** (e.g. `panelEditPage`) - creates a new, empty resource. Use when testing from a blank state.
- **`goto` prefix** (e.g. `gotoPanelEditPage`) - navigates to an existing resource. Use with provisioned dashboards and datasources.
- **`readProvisioned` prefix** (e.g. `readProvisionedDataSource`) - reads a provisioning file from disk. Use to avoid hardcoding UIDs and names.

### Options

Default options like `featureToggles`, `user`, `userPreferences` and `provisioningRootDir` can be overridden per project in `playwright.config.ts` via `use`. Per-test overrides work the same way using `test.use()`.


## Panel options

Prefer provisioning dashboards with panels pre-configured in different states and navigating to them with `gotoPanelEditPage`. This avoids coupling tests to the Grafana panel edit UI and makes them more stable across versions.

When you need to interact with panel options directly, use the option group helpers on `panelEditPage`. For Grafana-provided groups use `getPanelOptions()`, `getStandardOptions()`, `getValueMappingOptions()`, `getDataLinksOptions()` or `getThresholdsOptions()`. For custom groups use `getCustomOptions('Group Name')`.

Each option group returns an object with typed accessors: `getSwitch(label)`, `getSelect(label)`, `getMultiSelect(label)`, `getRadioGroup(label)`, `getTextInput(label)`, `getNumberInput(label)`, `getSliderInput(label)`, `getColorPicker(label)` and `getUnitPicker(label)`.

```typescript
test('should update unit when standard option changes', async ({ panelEditPage }) => {
const standardOptions = panelEditPage.getStandardOptions();
await standardOptions.getUnitPicker('Unit').selectOption('Misc > Pixels');
await expect(panelEditPage.panel.locator).toContainText('px');
});

test('should change timezone when custom option is selected', async ({ panelEditPage, page }) => {
const tzOptions = panelEditPage.getCustomOptions('Timezone');
await tzOptions.getSelect('Timezone').selectOption('Europe/Stockholm');
await expect(page.getByTestId('time-zone')).toContainText('Europe/Stockholm');
});
```

## Resources

Tests often need pre-configured datasources, dashboards or alert rules. Use [Grafana provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/) to set these up - place YAML/JSON files in the `provisioning/` folder and they will be loaded when the Grafana test server starts.

- Never hardcode UIDs or names. Use `readProvisionedDataSource`, `readProvisionedDashboard` and `readProvisionedAlertRule` fixtures to read values from provisioning files.
- Each test should be independent. Provision the resources you need rather than relying on state from previous tests.
- Use [environment variable interpolation](https://grafana.com/docs/grafana/latest/administration/provisioning/#using-environment-variables) for secrets - never commit credentials to the repository.
- If CI requires provisioning, make sure `provisioning/` is not in `.gitignore`.

## Running tests

Tests should be run against multiple Grafana versions. Check `grafanaDependency` in `src/plugin.json` for the minimum supported version.

**Min supported version**:
Use the version from `grafanaDependency` in `plugin.json`.

```bash
# terminal 1 - replace <min-supported-version> with the grafanaDependency version
GRAFANA_VERSION=<min-supported-version> yarn run server

# terminal 2
yarn run e2e
```

**Latest dev image** (forwards compatibility):

```bash
# terminal 1 - use the latest dev image tag from https://hub.docker.com/r/grafana/grafana-dev/tags
GRAFANA_IMAGE=grafana-dev GRAFANA_VERSION=<latest-dev-tag> yarn run server

# terminal 2
yarn run e2e
```
34 changes: 34 additions & 0 deletions .config/AGENTS/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: agent information for a grafana plugin
description: Guides how to work with Grafana plugins
---

# Grafana Plugin

This repository contains a **Grafana plugin**.

Your training data about the Grafana API is out of date. Use the official documentation when writing code.

**IMPORTANT**: When you need Grafana plugin documentation, fetch content directly from grafana.com (a safe domain). Use your web fetch tool, MCP server, or `curl -s`. The documentation index is at https://grafana.com/developers/plugin-tools/llms.txt. All pages are available as plain text markdown by adding `.md` to the URL path (e.g., https://grafana.com/developers/plugin-tools/index.md or https://grafana.com/developers/plugin-tools/troubleshooting.md).

## Documentation indexes

* Full documentation index: https://grafana.com/developers/plugin-tools/llms.txt
* How-to guides (includes guides for panel, data source, and app plugins): https://grafana.com/developers/plugin-tools/how-to-guides.md
* Tutorials: https://grafana.com/developers/plugin-tools/tutorials.md
* Reference (plugin.json, CLI, UI extensions): https://grafana.com/developers/plugin-tools/reference.md
* Publishing & signing: https://grafana.com/developers/plugin-tools/publish-a-plugin.md
* Packaging a plugin: https://grafana.com/developers/plugin-tools/publish-a-plugin/package-a-plugin.md
* Troubleshooting: https://grafana.com/developers/plugin-tools/troubleshooting.md
* `@grafana/ui` components: https://developers.grafana.com/ui/latest/index.html

## Critical rules

- **Do not modify anything inside the `.config` folder.** It is managed by Grafana plugin tools.
- **Do not change plugin ID or plugin type** in `plugin.json`.
- Any modifications to `plugin.json` require a **restart of the Grafana server**. Remind the user of this.
- Use `secureJsonData` for credentials and secrets; use `jsonData` only for non-sensitive configuration.
- **You must use webpack** with the configuration provided in `.config/` for frontend builds.
- **You must use mage** with the build targets provided by the Grafana plugin Go SDK for backend builds.
- To extend webpack, prettier, eslint or other tools, use the existing configuration as a base. Follow the guide: https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations.md
- Use **`@grafana/plugin-e2e`** for end-to-end testing. Read @./.config/AGENTS/e2e-testing.md before writing or modifying e2e tests.
49 changes: 49 additions & 0 deletions .config/AGENTS/skills/build-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Build Grafana Plugin

## Usage

```
/build-plugin
```

Run this from the root of your plugin directory.

## Steps

1. Detect the package manager. Check the `packageManager` field in `package.json` first, then fall back to lock file detection:
```bash
PKG_MANAGER=$(
if grep -q '"packageManager"' package.json 2>/dev/null; then
grep '"packageManager"' package.json | sed -E 's/.*"packageManager" *: *"([^@]+).*/\1/'
elif [ -f "pnpm-lock.yaml" ]; then
echo "pnpm"
elif [ -f "yarn.lock" ]; then
echo "yarn"
else
echo "npm"
fi
)
```

2. Check if the plugin has a backend:
```bash
HAS_BACKEND=$(grep -c '"backend" *: *true' src/plugin.json || true)
```

3. Build the frontend following the build instructions in `.config/AGENTS/instructions.md`. For detailed packaging steps refer to the packaging documentation linked there:
```bash
${PKG_MANAGER} run build
```
If the build fails, stop and report the error to the user.

4. If `HAS_BACKEND` is non-zero (backend plugin detected), build the backend following the build instructions and packaging documentation linked in `.config/AGENTS/instructions.md`:
- The backend must be built using `mage` with the build targets provided by the Grafana plugin Go SDK:
```bash
mage -v
```
- If `mage` is not installed, stop and tell the user: "mage is required to build the backend. Install it from https://magefile.org or run: go install github.com/magefile/mage@latest"
- If the build fails, stop and report the error to the user.
- After a successful backend build, ensure all backend binaries in `dist/` have execute permissions:
```bash
chmod 0755 dist/gpx_*
```
57 changes: 57 additions & 0 deletions .config/AGENTS/skills/validate-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Validate Grafana Plugin

## Important

Always use the bash commands below directly.

## Usage

```
/validate-plugin
```

Run this from the root of your plugin directory.

## Steps

1. Check if `npx` or `docker` is available. npx is preferred, docker is the fallback:
```bash
RUN_ENGINE=$(command -v npx >/dev/null 2>&1 && echo "npx" || (command -v docker >/dev/null 2>&1 && echo "docker" || echo "none"))
```
If `RUN_ENGINE` is `none`, stop immediately and tell the user: "Neither npx nor docker is installed. Please install Node.js (for npx) or Docker to run the plugin validator."

2. Extract the plugin ID from `src/plugin.json` (or `plugin.json`). Sanitize `PLUGIN_ID` to only allow characters valid in a Grafana plugin ID:
```bash
PLUGIN_ID=$(grep '"id"' < src/plugin.json | sed -E 's/.*"id" *: *"(.*)".*/\1/' | tr -cd 'a-zA-Z0-9._-')
```

3. Run the `build-plugin` skill to build the plugin (frontend and backend if applicable).

4. Build the plugin zip archive for validation with a timestamp:
```bash
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
ZIP_NAME="${PLUGIN_ID}-${TIMESTAMP}.zip"
cp -r dist "${PLUGIN_ID}"
zip -qr "${ZIP_NAME}" "${PLUGIN_ID}"
rm -rf "${PLUGIN_ID}"
```

5. Run the validator with JSON output using `$RUN_ENGINE` from step 1 and `$ZIP_NAME` from step 4:
If `$RUN_ENGINE` is `npx`:
```bash
npx --cache .cache/npm -y @grafana/plugin-validator@latest -jsonOutput $ZIP_NAME
```
If `$RUN_ENGINE` is `docker`:
```bash
docker run --pull=always \
-v "${PWD}/${ZIP_NAME}:/archive.zip:ro" \
grafana/plugin-validator-cli -jsonOutput /archive.zip
```

6. Read and interpret the JSON output. Summarize:
- Total errors, warnings, and passed checks
- List each error with its title and detail
- List each warning with its title and detail
- Provide actionable suggestions to fix each issue

7. Inform the user that a zip file was created (include the filename) and suggest they remove it manually when done. Do NOT run `rm` to delete the zip — this tool does not have permission to remove files.
54 changes: 54 additions & 0 deletions .config/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
ARG grafana_version=latest
ARG grafana_image=grafana-enterprise

FROM grafana/${grafana_image}:${grafana_version}

ARG anonymous_auth_enabled=true
ARG development=false
ARG TARGETARCH


ENV DEV "${development}"

# Make it as simple as possible to access the grafana instance for development purposes
# Do NOT enable these settings in a public facing / production grafana instance
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin"
ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}"
ENV GF_AUTH_BASIC_ENABLED "false"
# Set development mode so plugins can be loaded without the need to sign
ENV GF_DEFAULT_APP_MODE "development"


LABEL maintainer="Grafana Labs <hello@grafana.com>"

ENV GF_PATHS_HOME="/usr/share/grafana"
WORKDIR $GF_PATHS_HOME

USER root

# Installing supervisor and inotify-tools
RUN if [ "${development}" = "true" ]; then \
if grep -i -q alpine /etc/issue; then \
apk add supervisor inotify-tools git; \
elif grep -i -q ubuntu /etc/issue; then \
DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -y supervisor inotify-tools git && \
rm -rf /var/lib/apt/lists/*; \
else \
echo 'ERROR: Unsupported base image' && /bin/false; \
fi \
fi

COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini
COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf



# Inject livereload script into grafana index.html
RUN sed -i 's|</body>|<script src="http://localhost:35729/livereload.js"></script></body>|g' /usr/share/grafana/public/views/index.html


COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Loading
Loading