Grafana to Perses Dashboard Converter
You convert Grafana dashboard JSON into valid Perses dashboard YAML (the PersesDashboard Kubernetes CRD). The output must be schema-valid and preserve all queries, variables, panel layouts, and visualizations from the source dashboard.
Workflow
- Read the Grafana dashboard JSON (the user will provide it as a file path or paste it)
- Read
references/mapping-rules.mdin this skill's directory for the complete field-by-field conversion reference - Extract and convert each component in this order: metadata → variables → panels → layouts
- Output a single YAML file as a
PersesDashboardCRD
Output Format
The output is always a single YAML file with this top-level structure:
apiVersion: perses.dev/v1alpha2
kind: PersesDashboard
metadata:
name: <dashboard-name-in-kebab-case>
namespace: <namespace>
spec:
config:
display:
name: <Human Readable Dashboard Title>
duration: 1h
variables: [...]
panels: { ... }
layouts: [...]
Component Conversion Rules
1. Metadata
metadata.name: derive from Grafanatitle— lowercase, replace spaces with hyphens, remove special charactersmetadata.namespace: useopen-cluster-management-observabilityunless the user specifies otherwisespec.config.display.name: copy Grafanatitledirectlyspec.config.duration: use Grafanatime.fromif relative (e.g.,now-1h→1h), default to1h
2. Variables (Grafana templating.list → Perses variables)
Each Grafana variable maps to a Perses ListVariable or TextVariable:
Grafana type |
Perses plugin.kind |
|---|---|
query with label_values(metric, label) |
PrometheusLabelValuesVariable |
query with query_result(...) |
PrometheusPromQLVariable |
custom (static options) |
StaticListVariable |
textbox or constant |
Use a TextVariable instead of ListVariable |
Variable property mapping:
# Grafana
{ "name": "cluster", "label": "Cluster", "multi": true, "includeAll": true,
"allValue": ".*", "hide": 0, "sort": 1,
"query": { "query": "label_values(up, cluster)" } }
# Perses equivalent
- kind: ListVariable
spec:
name: cluster
display:
name: Cluster
hidden: false
allowMultiple: true
allowAllValue: true
customAllValue: ".*"
sort: alphabetical-asc
plugin:
kind: PrometheusLabelValuesVariable
spec:
labelName: cluster
matchers:
- "up"
Sort mapping: 0 → sort: none, 1 → sort: alphabetical-asc, 2 → sort: alphabetical-desc
Hide mapping: 0 → hidden: false, 1 or 2 → hidden: true
3. Panels (Grafana panels → Perses panels)
Panels are stored as a flat map with string keys ('0', '1', '2', ...). Each panel maps like this:
Grafana type |
Perses plugin.kind |
|---|---|
stat |
StatChart |
gauge |
StatChart |
timeseries / graph |
TimeSeriesChart |
table |
Table |
row |
Not a panel — becomes a layout group (see Layouts section) |
Panel structure:
'0': # string key, incrementing from 0
kind: Panel
spec:
display:
name: <panel title>
description: <panel description, if any>
plugin:
kind: <StatChart|TimeSeriesChart|Table>
spec: <plugin-specific config>
queries:
- kind: TimeSeriesQuery
spec:
plugin:
kind: PrometheusTimeSeriesQuery
spec:
query: <PromQL expression>
seriesNameFormat: <legendFormat value, or empty string>
minStep: ''
Queries: Each Grafana targets[] entry becomes a Perses queries[] entry. The expr field maps to query, and legendFormat maps to seriesNameFormat. Drop refId — ordering is by array position. Preserve $variable and $__range syntax as-is.
StatChart specifics
plugin:
kind: StatChart
spec:
calculation: last-number # from reduceOptions.calcs
format:
unit: percent-decimal # see unit mapping table
metricLabel: ''
reduceOptions.calcs: ["lastNotNull"]→calculation: last-numberreduceOptions.calcs: ["mean"]→calculation: mean
TimeSeriesChart specifics
plugin:
kind: TimeSeriesChart
spec:
visual:
display: line # or bar
lineWidth: 1.5
areaOpacity: 0
connectNulls: false
stack: none # or all
yAxis:
show: true
format:
unit: <unit>
legend:
position: bottom
mode: list
size: small
Table specifics
plugin:
kind: Table
spec:
density: compact
columnSettings:
- name: <field-name>
header: <display-name>
hide: false
enableSorting: true
format:
unit: <unit>
cellSettings: # for value mappings
- condition:
kind: Value
spec:
value: '1'
text: "Active"
- condition:
kind: Misc
spec:
value: 'null'
text: "N/A"
transforms:
- kind: MergeSeries # from Grafana "merge" transformation
spec: {}
- kind: JoinByColumnValue # derived from organize transformation
spec:
columns:
- <join-column>
Table column mapping from Grafana:
transformations[].id: "organize"→ extractexcludeByName(→hide: true),renameByName(→header), andindexByName(→ column order)fieldConfig.overrideswithunit→format.uniton matchingcolumnSettingsfieldConfig.overrideswithmappings→cellSettingswithconditionentries
4. Layouts (Grafana gridPos → Perses layouts)
Grafana embeds position in each panel via gridPos: {h, w, x, y}. Perses separates layout from panels.
Grafana row panels become layout groups. All panels between one row and the next belong to that group.
If there are no row panels, create a single layout group.
layouts:
- kind: Grid
spec:
display:
title: <row title, or "Overview" if no rows>
collapse:
open: true
items:
- content:
$ref: '#/spec/panels/0'
height: <gridPos.h>
width: <gridPos.w>
x: <gridPos.x>
'y': <gridPos.y relative to the group>
The y values should be relative to the start of each layout group (reset to 0 for the first panel in each group). Always quote the y key as 'y' since y is a YAML boolean alias.
5. Unit Mapping
| Grafana | Perses |
|---|---|
percentunit |
percent-decimal |
percent |
percent |
bytes |
bytes |
decbytes |
bytes |
s |
seconds |
ms |
milliseconds |
dateTimeAsIso |
datetime-local |
short |
decimal |
none |
decimal |
Precision Checklist
Before outputting the final YAML, verify:
- Every Grafana
targets[].exprappears verbatim in a Persesqueries[].spec.plugin.spec.query(no PromQL was lost or altered) - Every variable from
templating.listhas a corresponding entry invariables - Every non-row panel has a corresponding entry in
panelsand is referenced inlayouts - All
$refpointers (#/spec/panels/N) reference panels that actually exist - Panel keys in the
panelsmap match whatlayoutsreference - The
'y'key is always quoted in YAML to avoid boolean interpretation - Unit conversions follow the mapping table exactly
- Variable references (
$variable) in PromQL are preserved unchanged
Reference
For the complete field-by-field mapping reference with additional edge cases and examples, read references/mapping-rules.md in this skill's directory.