Compare commits
5 Commits
gamab/auth
...
feat/865-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7e052723 | ||
|
|
b1057b3cbf | ||
|
|
a79cda3328 | ||
|
|
65cdf6cd45 | ||
|
|
7be93d9af4 |
287
apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.legacy-ds-ref.json
vendored
Normal file
287
apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.legacy-ds-ref.json
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v1beta1",
|
||||
"metadata": {
|
||||
"name": "legacy-ds-ref"
|
||||
},
|
||||
"spec": {
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Minimum cluster size"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [10, 10],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 16,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"editorMode": "code",
|
||||
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
|
||||
"instant": false,
|
||||
"legendFormat": "{{version}}",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Number of Alloy Instances",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"Total"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": true,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 0
|
||||
},
|
||||
"id": 17,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
|
||||
"hide": true,
|
||||
"instant": false,
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
|
||||
"instant": false,
|
||||
"legendFormat": "Total",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "CPU usage",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-90m",
|
||||
"to": "now"
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Legacy DS Panel Query Ref",
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
294
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v0alpha1.json
vendored
Normal file
294
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v0alpha1.json
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"name": "legacy-ds-ref"
|
||||
},
|
||||
"spec": {
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Minimum cluster size"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 16,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"editorMode": "code",
|
||||
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
|
||||
"instant": false,
|
||||
"legendFormat": "{{version}}",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Number of Alloy Instances",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"Total"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": true,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 0
|
||||
},
|
||||
"id": 17,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
|
||||
"hide": true,
|
||||
"instant": false,
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": "${datasource}",
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
|
||||
"instant": false,
|
||||
"legendFormat": "Total",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "CPU usage",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-90m",
|
||||
"to": "now"
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Legacy DS Panel Query Ref",
|
||||
"weekStart": ""
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
405
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v2alpha1.json
vendored
Normal file
405
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v2alpha1.json
vendored
Normal file
@@ -0,0 +1,405 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2alpha1",
|
||||
"metadata": {
|
||||
"name": "legacy-ds-ref"
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [
|
||||
{
|
||||
"kind": "AnnotationQuery",
|
||||
"spec": {
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"query": {
|
||||
"kind": "grafana",
|
||||
"spec": {}
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"builtIn": true,
|
||||
"legacyOptions": {
|
||||
"type": "dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"cursorSync": "Off",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-16": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 16,
|
||||
"title": "Number of Alloy Instances",
|
||||
"description": "",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "",
|
||||
"spec": {
|
||||
"editorMode": "code",
|
||||
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
|
||||
"instant": false,
|
||||
"legendFormat": "{{version}}",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "timeseries",
|
||||
"spec": {
|
||||
"pluginVersion": "",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Minimum cluster size"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-17": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 17,
|
||||
"title": "CPU usage",
|
||||
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "",
|
||||
"spec": {
|
||||
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
|
||||
"instant": false,
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "",
|
||||
"spec": {
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
|
||||
"instant": false,
|
||||
"legendFormat": "Total",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "timeseries",
|
||||
"spec": {
|
||||
"pluginVersion": "",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percentunit",
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"Total"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": true,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 8,
|
||||
"height": 9,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-16"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"width": 8,
|
||||
"height": 9,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-17"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [],
|
||||
"timeSettings": {
|
||||
"timezone": "utc",
|
||||
"from": "now-90m",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "Legacy DS Panel Query Ref",
|
||||
"variables": []
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
411
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v2beta1.json
vendored
Normal file
411
apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.legacy-ds-ref.v2beta1.json
vendored
Normal file
@@ -0,0 +1,411 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"apiVersion": "dashboard.grafana.app/v2beta1",
|
||||
"metadata": {
|
||||
"name": "legacy-ds-ref"
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [
|
||||
{
|
||||
"kind": "AnnotationQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "grafana",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Grafana --"
|
||||
},
|
||||
"spec": {}
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"builtIn": true,
|
||||
"legacyOptions": {
|
||||
"type": "dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"cursorSync": "Off",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-16": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 16,
|
||||
"title": "Number of Alloy Instances",
|
||||
"description": "",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "${datasource}"
|
||||
},
|
||||
"spec": {
|
||||
"editorMode": "code",
|
||||
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
|
||||
"instant": false,
|
||||
"legendFormat": "{{version}}",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "timeseries",
|
||||
"version": "",
|
||||
"spec": {
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Minimum cluster size"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-17": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 17,
|
||||
"title": "CPU usage",
|
||||
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "${datasource}"
|
||||
},
|
||||
"spec": {
|
||||
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
|
||||
"instant": false,
|
||||
"legendFormat": "{{instance}}",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "${datasource}"
|
||||
},
|
||||
"spec": {
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
|
||||
"instant": false,
|
||||
"legendFormat": "Total",
|
||||
"range": true
|
||||
}
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "timeseries",
|
||||
"version": "",
|
||||
"spec": {
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"timeCompare": false,
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percentunit",
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"value": 80,
|
||||
"color": "red"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"Total"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": true,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 8,
|
||||
"height": 9,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-16"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"width": 8,
|
||||
"height": 9,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-17"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [],
|
||||
"timeSettings": {
|
||||
"timezone": "utc",
|
||||
"from": "now-90m",
|
||||
"to": "now",
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"hideTimepicker": false,
|
||||
"fiscalYearStartMonth": 0
|
||||
},
|
||||
"title": "Legacy DS Panel Query Ref",
|
||||
"variables": []
|
||||
},
|
||||
"status": {
|
||||
"conversion": {
|
||||
"failed": false,
|
||||
"storedVersion": "v1beta1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,11 @@ func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard,
|
||||
// Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings
|
||||
normalizeTemplateVariableDatasources(out.Spec.Object)
|
||||
|
||||
// Normalize panel and target datasources from string to object format
|
||||
// This handles legacy dashboards where panels/targets have datasource: "$datasource" (string)
|
||||
// instead of datasource: { uid: "$datasource" } (object)
|
||||
normalizePanelDatasources(out.Spec.Object)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -134,3 +139,62 @@ func isTemplateVariableRef(s string) bool {
|
||||
}
|
||||
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
|
||||
}
|
||||
|
||||
// normalizePanelDatasources converts panel and target string datasources to object format.
|
||||
// Legacy dashboards may have panels/targets with datasource: "$datasource" (string).
|
||||
// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion.
|
||||
func normalizePanelDatasources(dashboard map[string]interface{}) {
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
normalizePanelsDatasources(panels)
|
||||
}
|
||||
|
||||
// normalizePanelsDatasources normalizes datasources in a list of panels (including nested row panels)
|
||||
func normalizePanelsDatasources(panels []interface{}) {
|
||||
for _, panel := range panels {
|
||||
panelMap, ok := panel.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle row panels with nested panels
|
||||
if panelType, _ := panelMap["type"].(string); panelType == "row" {
|
||||
if nestedPanels, ok := panelMap["panels"].([]interface{}); ok {
|
||||
normalizePanelsDatasources(nestedPanels)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize panel-level datasource
|
||||
if ds := panelMap["datasource"]; ds != nil {
|
||||
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
|
||||
panelMap["datasource"] = map[string]interface{}{
|
||||
"uid": dsStr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize target-level datasources
|
||||
targets, ok := panelMap["targets"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
targetMap, ok := target.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if ds := targetMap["datasource"]; ds != nil {
|
||||
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
|
||||
targetMap["datasource"] = map[string]interface{}{
|
||||
"uid": dsStr,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2059,6 +2059,12 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{},
|
||||
Uid: &dsUID,
|
||||
}
|
||||
}
|
||||
} else if dsStr, ok := ds.(string); ok && isTemplateVariable(dsStr) {
|
||||
// Handle legacy panel datasource as string (template variable reference e.g., "$datasource")
|
||||
// Only process template variables - other string values are not supported in V2 format
|
||||
panelDatasource = &dashv2alpha1.DashboardDataSourceRef{
|
||||
Uid: &dsStr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2145,6 +2151,10 @@ func transformSingleQuery(ctx context.Context, targetMap map[string]interface{},
|
||||
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
|
||||
queryDatasourceUID = resolveGrafanaDatasourceUID(queryDatasourceType, queryDatasourceUID)
|
||||
}
|
||||
} else if dsStr, ok := targetMap["datasource"].(string); ok && isTemplateVariable(dsStr) {
|
||||
// Handle legacy target datasource as string (template variable reference e.g., "$datasource")
|
||||
// Only process template variables - other string values are not supported in V2 format
|
||||
queryDatasourceUID = dsStr
|
||||
}
|
||||
|
||||
// Use panel datasource if target datasource is missing or empty
|
||||
|
||||
@@ -116,3 +116,26 @@ type ConnectionList struct {
|
||||
// +listType=atomic
|
||||
Items []Connection `json:"items"`
|
||||
}
|
||||
|
||||
// ExternalRepositoryList lists repositories from an external git provider
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ExternalRepositoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +listType=atomic
|
||||
Items []ExternalRepository `json:"items"`
|
||||
}
|
||||
|
||||
type ExternalRepository struct {
|
||||
// Name of the repository
|
||||
Name string `json:"name"`
|
||||
// Owner is the user, organization, or workspace that owns the repository
|
||||
// For GitHub: organization or user
|
||||
// For GitLab: namespace (user or group)
|
||||
// For Bitbucket: workspace
|
||||
// For pure Git: empty
|
||||
Owner string `json:"owner,omitempty"`
|
||||
// URL of the repository
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
||||
&HistoricJobList{},
|
||||
&Connection{},
|
||||
&ConnectionList{},
|
||||
&ExternalRepositoryList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -262,6 +262,53 @@ func (in *ExportJobOptions) DeepCopy() *ExportJobOptions {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalRepository) DeepCopyInto(out *ExternalRepository) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepository.
|
||||
func (in *ExternalRepository) DeepCopy() *ExternalRepository {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExternalRepository)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalRepositoryList) DeepCopyInto(out *ExternalRepositoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ExternalRepository, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalRepositoryList.
|
||||
func (in *ExternalRepositoryList) DeepCopy() *ExternalRepositoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExternalRepositoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ExternalRepositoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FileItem) DeepCopyInto(out *FileItem) {
|
||||
*out = *in
|
||||
|
||||
@@ -26,6 +26,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.DeleteJobOptions": schema_pkg_apis_provisioning_v0alpha1_DeleteJobOptions(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository": schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepositoryList": schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref),
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.GitHubConnectionConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubConnectionConfig(ref),
|
||||
@@ -544,6 +546,96 @@ func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.Reference
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepository(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Name of the repository",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"owner": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"url": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "URL of the repository",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "url"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_ExternalRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ExternalRepositoryList lists repositories from an external git provider",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1.ExternalRepository", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_FileItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Paths
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,DeleteJobOptions,Resources
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ExternalRepositoryList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,FileList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,HistoryList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobResourceSummary,Errors
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"@grafana/api-clients": "12.4.0-pre",
|
||||
"@grafana/i18n": "12.4.0-pre",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@reduxjs/toolkit": "2.10.1",
|
||||
"fishery": "^2.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"tinycolor2": "^1.6.0"
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": ">=11.6 <= 12.x",
|
||||
"@reduxjs/toolkit": "^2.8.0",
|
||||
"@reduxjs/toolkit": "^2.10.0",
|
||||
"rxjs": "7.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['Connection'],
|
||||
}),
|
||||
getConnectionRepositories: build.query<GetConnectionRepositoriesApiResponse, GetConnectionRepositoriesApiArg>({
|
||||
query: (queryArg) => ({ url: `/connections/${queryArg.name}/repositories` }),
|
||||
providesTags: ['Connection'],
|
||||
}),
|
||||
getConnectionStatus: build.query<GetConnectionStatusApiResponse, GetConnectionStatusApiArg>({
|
||||
query: (queryArg) => ({
|
||||
url: `/connections/${queryArg.name}/status`,
|
||||
@@ -726,6 +730,18 @@ export type UpdateConnectionApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetConnectionRepositoriesApiResponse = /** status 200 OK */ {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion?: string;
|
||||
items: any[];
|
||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||
kind?: string;
|
||||
metadata?: any;
|
||||
};
|
||||
export type GetConnectionRepositoriesApiArg = {
|
||||
/** name of the ExternalRepositoryList */
|
||||
name: string;
|
||||
};
|
||||
export type GetConnectionStatusApiResponse = /** status 200 OK */ Connection;
|
||||
export type GetConnectionStatusApiArg = {
|
||||
/** name of the Connection */
|
||||
@@ -2079,6 +2095,8 @@ export const {
|
||||
useReplaceConnectionMutation,
|
||||
useDeleteConnectionMutation,
|
||||
useUpdateConnectionMutation,
|
||||
useGetConnectionRepositoriesQuery,
|
||||
useLazyGetConnectionRepositoriesQuery,
|
||||
useGetConnectionStatusQuery,
|
||||
useLazyGetConnectionStatusQuery,
|
||||
useReplaceConnectionStatusMutation,
|
||||
|
||||
@@ -599,6 +599,7 @@ export {
|
||||
type PluginExtensionResourceAttributesContext,
|
||||
type CentralAlertHistorySceneV1Props,
|
||||
} from './types/pluginExtensions';
|
||||
export { type PrometheusQueryResultsV1Props } from './types/exposedComponentProps';
|
||||
export {
|
||||
type ScopeDashboardBindingSpec,
|
||||
type ScopeDashboardBindingStatus,
|
||||
|
||||
27
packages/grafana-data/src/types/exposedComponentProps.ts
Normal file
27
packages/grafana-data/src/types/exposedComponentProps.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { LoadingState } from './data';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { DataLinkPostProcessor } from './fieldOverrides';
|
||||
import { TimeZone } from './time';
|
||||
|
||||
/**
|
||||
* Props for the PrometheusQueryResults exposed component.
|
||||
* @see PluginExtensionExposedComponents.PrometheusQueryResultsV1
|
||||
*/
|
||||
export type PrometheusQueryResultsV1Props = {
|
||||
/** Raw DataFrames to display (processing handled internally). Defaults to empty array. */
|
||||
tableResult?: DataFrame[];
|
||||
/** Width of the container in pixels. Defaults to 800. */
|
||||
width?: number;
|
||||
/** Timezone for value formatting. Defaults to 'browser'. */
|
||||
timeZone?: TimeZone;
|
||||
/** Loading state for panel chrome indicator */
|
||||
loading?: LoadingState;
|
||||
/** Aria label for accessibility */
|
||||
ariaLabel?: string;
|
||||
/** Start in Raw view instead of Table view. When true, shows toggle. */
|
||||
showRawPrometheus?: boolean;
|
||||
/** Callback when user adds a cell filter */
|
||||
onCellFilterAdded?: (filter: { key: string; value: string; operator: '=' | '!=' }) => void;
|
||||
/** Optional post-processor for data links (used by Explore for split view) */
|
||||
dataLinkPostProcessor?: DataLinkPostProcessor;
|
||||
};
|
||||
@@ -245,6 +245,7 @@ export enum PluginExtensionPointPatterns {
|
||||
export enum PluginExtensionExposedComponents {
|
||||
CentralAlertHistorySceneV1 = 'grafana/central-alert-history-scene/v1',
|
||||
AddToDashboardFormV1 = 'grafana/add-to-dashboard-form/v1',
|
||||
PrometheusQueryResultsV1 = 'grafana/prometheus-query-results/v1',
|
||||
}
|
||||
|
||||
export type PluginExtensionPanelContext = {
|
||||
|
||||
@@ -170,56 +170,42 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
|
||||
if !ok {
|
||||
return nil, storewrapper.ErrUnauthenticated
|
||||
}
|
||||
r.logger.Debug("filtering resource permissions list with auth info",
|
||||
"namespace", authInfo.GetNamespace(),
|
||||
"identity Subject", authInfo.GetSubject(),
|
||||
"identity UID", authInfo.GetUID(),
|
||||
"identity type", authInfo.GetIdentityType(),
|
||||
)
|
||||
|
||||
switch l := list.(type) {
|
||||
case *iamv0.ResourcePermissionList:
|
||||
|
||||
r.logger.Debug("filtering list of length", "length", len(l.Items))
|
||||
var (
|
||||
filteredItems []iamv0.ResourcePermission
|
||||
err error
|
||||
canViewFuncs = map[schema.GroupResource]types.ItemChecker{}
|
||||
)
|
||||
for _, item := range l.Items {
|
||||
target := item.Spec.Resource
|
||||
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
|
||||
|
||||
r.logger.Debug("target resource",
|
||||
"group", target.ApiGroup,
|
||||
"resource", target.Resource,
|
||||
"name", target.Name,
|
||||
)
|
||||
gr := schema.GroupResource{
|
||||
Group: item.Spec.Resource.ApiGroup,
|
||||
Resource: item.Spec.Resource.Resource,
|
||||
}
|
||||
|
||||
// Reuse the same canView for items with the same resource
|
||||
canView, found := canViewFuncs[targetGR]
|
||||
canView, found := canViewFuncs[gr]
|
||||
|
||||
if !found {
|
||||
listReq := types.ListRequest{
|
||||
Namespace: item.Namespace,
|
||||
Group: target.ApiGroup,
|
||||
Resource: target.Resource,
|
||||
Group: item.Spec.Resource.ApiGroup,
|
||||
Resource: item.Spec.Resource.Resource,
|
||||
Verb: utils.VerbGetPermissions,
|
||||
}
|
||||
r.logger.Debug("compiling list request",
|
||||
"namespace", item.Namespace,
|
||||
"group", target.ApiGroup,
|
||||
"resource", target.Resource,
|
||||
"verb", utils.VerbGetPermissions,
|
||||
)
|
||||
|
||||
canView, _, err = r.accessClient.Compile(ctx, authInfo, listReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canViewFuncs[targetGR] = canView
|
||||
canViewFuncs[gr] = canView
|
||||
}
|
||||
|
||||
target := item.Spec.Resource
|
||||
targetGR := schema.GroupResource{Group: target.ApiGroup, Resource: target.Resource}
|
||||
|
||||
parent := ""
|
||||
// Fetch the parent of the resource
|
||||
// It's not efficient to do for every item in the list, but it's a good starting point.
|
||||
@@ -237,13 +223,6 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
|
||||
)
|
||||
continue
|
||||
}
|
||||
r.logger.Debug("fetched parent",
|
||||
"parent", p,
|
||||
"namespace", item.Namespace,
|
||||
"group", target.ApiGroup,
|
||||
"resource", target.Resource,
|
||||
"name", target.Name,
|
||||
)
|
||||
parent = p
|
||||
}
|
||||
|
||||
|
||||
69
pkg/registry/apis/provisioning/connection_repositories.go
Normal file
69
pkg/registry/apis/provisioning/connection_repositories.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type connectionRepositoriesConnector struct{}
|
||||
|
||||
func NewConnectionRepositoriesConnector() *connectionRepositoriesConnector {
|
||||
return &connectionRepositoriesConnector{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) New() runtime.Object {
|
||||
return &provisioning.ExternalRepositoryList{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) Destroy() {}
|
||||
|
||||
func (*connectionRepositoriesConnector) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) ProducesObject(verb string) any {
|
||||
return &provisioning.ExternalRepositoryList{}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) ConnectMethods() []string {
|
||||
return []string{http.MethodGet}
|
||||
}
|
||||
|
||||
func (*connectionRepositoriesConnector) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (c *connectionRepositoriesConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
logger := logging.FromContext(ctx).With("logger", "connection-repositories-connector", "connection_name", name)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), r.Method))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("repositories endpoint called but not yet implemented")
|
||||
|
||||
// TODO: Implement repository listing from external git provider
|
||||
// This will require:
|
||||
// 1. Get the Connection object using logging.Context(r.Context(), logger)
|
||||
// 2. Use the connection credentials to authenticate with the git provider
|
||||
// 3. List repositories from the provider (GitHub, GitLab, Bitbucket)
|
||||
// 4. Return ExternalRepositoryList with Name, Owner, and URL for each repository
|
||||
|
||||
responder.Error(apierrors.NewMethodNotSupported(provisioning.ConnectionResourceInfo.GroupResource(), "repositories endpoint not yet implemented"))
|
||||
}), nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*connectionRepositoriesConnector)(nil)
|
||||
_ rest.Connecter = (*connectionRepositoriesConnector)(nil)
|
||||
_ rest.StorageMetadata = (*connectionRepositoriesConnector)(nil)
|
||||
)
|
||||
101
pkg/registry/apis/provisioning/connection_repositories_test.go
Normal file
101
pkg/registry/apis/provisioning/connection_repositories_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
func TestConnectionRepositoriesConnector(t *testing.T) {
|
||||
connector := NewConnectionRepositoriesConnector()
|
||||
|
||||
t.Run("New returns ExternalRepositoryList", func(t *testing.T) {
|
||||
obj := connector.New()
|
||||
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
|
||||
})
|
||||
|
||||
t.Run("ProducesMIMETypes returns application/json", func(t *testing.T) {
|
||||
types := connector.ProducesMIMETypes("GET")
|
||||
require.Equal(t, []string{"application/json"}, types)
|
||||
})
|
||||
|
||||
t.Run("ProducesObject returns ExternalRepositoryList", func(t *testing.T) {
|
||||
obj := connector.ProducesObject("GET")
|
||||
require.IsType(t, &provisioning.ExternalRepositoryList{}, obj)
|
||||
})
|
||||
|
||||
t.Run("ConnectMethods returns GET", func(t *testing.T) {
|
||||
methods := connector.ConnectMethods()
|
||||
require.Equal(t, []string{http.MethodGet}, methods)
|
||||
})
|
||||
|
||||
t.Run("NewConnectOptions returns no path component", func(t *testing.T) {
|
||||
obj, hasPath, path := connector.NewConnectOptions()
|
||||
require.Nil(t, obj)
|
||||
require.False(t, hasPath)
|
||||
require.Empty(t, path)
|
||||
})
|
||||
|
||||
t.Run("Connect returns handler that rejects non-GET methods", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
responder := &mockResponder{}
|
||||
|
||||
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test POST method (should be rejected)
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.True(t, responder.called)
|
||||
require.NotNil(t, responder.err)
|
||||
require.True(t, apierrors.IsMethodNotSupported(responder.err))
|
||||
})
|
||||
|
||||
t.Run("Connect returns handler that returns not implemented for GET", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
responder := &mockResponder{}
|
||||
|
||||
handler, err := connector.Connect(ctx, "test-connection", nil, responder)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test GET method (should return not implemented)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.True(t, responder.called)
|
||||
require.NotNil(t, responder.err)
|
||||
require.True(t, apierrors.IsMethodNotSupported(responder.err))
|
||||
require.Contains(t, responder.err.Error(), "not yet implemented")
|
||||
})
|
||||
}
|
||||
|
||||
// mockResponder implements rest.Responder for testing
|
||||
type mockResponder struct {
|
||||
called bool
|
||||
err error
|
||||
obj runtime.Object
|
||||
code int
|
||||
}
|
||||
|
||||
func (m *mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||
m.called = true
|
||||
m.code = statusCode
|
||||
m.obj = obj
|
||||
}
|
||||
|
||||
func (m *mockResponder) Error(err error) {
|
||||
m.called = true
|
||||
m.err = err
|
||||
}
|
||||
@@ -480,6 +480,16 @@ func (b *APIBuilder) authorizeConnectionSubresource(ctx context.Context, a autho
|
||||
Namespace: a.GetNamespace(),
|
||||
}, ""))
|
||||
|
||||
// Repositories is read-only
|
||||
case "repositories":
|
||||
return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{
|
||||
Verb: apiutils.VerbGet,
|
||||
Group: provisioning.GROUP,
|
||||
Resource: provisioning.ConnectionResourceInfo.GetName(),
|
||||
Name: a.GetName(),
|
||||
Namespace: a.GetNamespace(),
|
||||
}, ""))
|
||||
|
||||
default:
|
||||
id, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
@@ -603,6 +613,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
|
||||
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath()] = connectionsStore
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath("status")] = connectionStatusStorage
|
||||
storage[provisioning.ConnectionResourceInfo.StoragePath("repositories")] = NewConnectionRepositoriesConnector()
|
||||
|
||||
// TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories))
|
||||
@@ -1247,6 +1258,23 @@ spec:
|
||||
oas.Paths.Paths[repoprefix+"/jobs/{uid}"] = sub
|
||||
}
|
||||
|
||||
// Document connection repositories endpoint
|
||||
connectionprefix := root + "namespaces/{namespace}/connections/{name}"
|
||||
sub = oas.Paths.Paths[connectionprefix+"/repositories"]
|
||||
if sub != nil {
|
||||
sub.Get.Description = "List repositories available from the external git provider through this connection"
|
||||
sub.Get.Summary = "List external repositories"
|
||||
sub.Get.Parameters = []*spec3.Parameter{}
|
||||
sub.Post = nil
|
||||
sub.Put = nil
|
||||
sub.Delete = nil
|
||||
|
||||
// Replace the content type for this response
|
||||
mt := sub.Get.Responses.StatusCodeResponses[200].Content
|
||||
s := defs[defsBase+"ExternalRepositoryList"].Schema
|
||||
mt["*/*"].Schema = &s
|
||||
}
|
||||
|
||||
// Run all extra post-processors.
|
||||
for _, extra := range b.extras {
|
||||
if err := extra.PostProcessOpenAPI(oas); err != nil {
|
||||
|
||||
@@ -866,6 +866,80 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/repositories": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Connection"
|
||||
],
|
||||
"summary": "List external repositories",
|
||||
"description": "List repositories available from the external git provider through this connection",
|
||||
"operationId": "getConnectionRepositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"description": "ExternalRepositoryList lists repositories from an external git provider",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items"
|
||||
],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {}
|
||||
},
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "connect",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "provisioning.grafana.app",
|
||||
"version": "v0alpha1",
|
||||
"kind": "ExternalRepositoryList"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "name of the ExternalRepositoryList",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"description": "object name and auth scope, such as for teams and projects",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4645,6 +4719,73 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the repository",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"owner": {
|
||||
"description": "Owner is the user, organization, or workspace that owns the repository For GitHub: organization or user For GitLab: namespace (user or group) For Bitbucket: workspace For pure Git: empty",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL of the repository",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepositoryList": {
|
||||
"description": "ExternalRepositoryList lists repositories from an external git provider",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items"
|
||||
],
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.ExternalRepository"
|
||||
}
|
||||
]
|
||||
},
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "provisioning.grafana.app",
|
||||
"kind": "ExternalRepositoryList",
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.FileItem": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
172
pkg/tests/apis/provisioning/connection_repositories_test.go
Normal file
172
pkg/tests/apis/provisioning/connection_repositories_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_ConnectionRepositories(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
|
||||
// Create a connection for testing
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "connection-repositories-test",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "123456",
|
||||
"installationID": "454545",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "someSecret",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
|
||||
require.NoError(t, err, "failed to create connection")
|
||||
|
||||
t.Run("endpoint returns not implemented", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).
|
||||
StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "should return error for not implemented endpoint")
|
||||
require.Equal(t, http.StatusMethodNotAllowed, statusCode, "should return 405 Method Not Allowed")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
})
|
||||
|
||||
t.Run("admin can access endpoint (gets not implemented)", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
// Endpoint exists but returns not implemented
|
||||
require.Error(t, result.Error(), "should return error")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
// Status code should be 405 (Method Not Allowed) for method not supported
|
||||
require.Equal(t, http.StatusMethodNotAllowed, statusCode)
|
||||
})
|
||||
|
||||
t.Run("editor cannot access endpoint", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.EditorREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "editor should not be able to access repositories endpoint")
|
||||
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
|
||||
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
|
||||
})
|
||||
|
||||
t.Run("viewer cannot access endpoint", func(t *testing.T) {
|
||||
var statusCode int
|
||||
result := helper.ViewerREST.Get().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "viewer should not be able to access repositories endpoint")
|
||||
require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden")
|
||||
require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden")
|
||||
})
|
||||
|
||||
t.Run("non-GET methods are rejected", func(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(map[string]any{})
|
||||
|
||||
var statusCode int
|
||||
result := helper.AdminREST.Post().
|
||||
Namespace("default").
|
||||
Resource("connections").
|
||||
Name("connection-repositories-test").
|
||||
SubResource("repositories").
|
||||
Body(configBytes).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
Do(ctx).StatusCode(&statusCode)
|
||||
|
||||
require.Error(t, result.Error(), "POST should not be allowed")
|
||||
require.True(t, apierrors.IsMethodNotSupported(result.Error()), "error should be MethodNotSupported")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_ConnectionRepositoriesResponseType(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
helper := runGrafana(t)
|
||||
ctx := context.Background()
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
|
||||
// Create a connection for testing
|
||||
connection := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "provisioning.grafana.app/v0alpha1",
|
||||
"kind": "Connection",
|
||||
"metadata": map[string]any{
|
||||
"name": "connection-repositories-type-test",
|
||||
"namespace": "default",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"type": "github",
|
||||
"github": map[string]any{
|
||||
"appID": "123456",
|
||||
"installationID": "454545",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"privateKey": map[string]any{
|
||||
"create": "someSecret",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
|
||||
require.NoError(t, err, "failed to create connection")
|
||||
|
||||
t.Run("verify ExternalRepositoryList type exists in API", func(t *testing.T) {
|
||||
// Verify the type is registered and can be instantiated
|
||||
list := &provisioning.ExternalRepositoryList{}
|
||||
require.NotNil(t, list)
|
||||
// Verify it has the expected structure (Items is a slice, nil by default is fine)
|
||||
require.IsType(t, []provisioning.ExternalRepository{}, list.Items)
|
||||
// Can create items
|
||||
list.Items = []provisioning.ExternalRepository{
|
||||
{Name: "test", Owner: "owner", URL: "https://example.com/repo"},
|
||||
}
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, "test", list.Items[0].Name)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
||||
import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame, LoadingState } from '@grafana/data';
|
||||
import { FieldType, InternalTimeZones, toDataFrame, LoadingState } from '@grafana/data';
|
||||
import { getTemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TABLE_RESULTS_STYLE } from 'app/types/explore';
|
||||
|
||||
import { RawPrometheusContainer } from './RawPrometheusContainer';
|
||||
import { PrometheusQueryResultsContainer } from './PrometheusQueryResultsContainer';
|
||||
|
||||
function getTable(): HTMLElement {
|
||||
return screen.getAllByRole('table')[0];
|
||||
@@ -52,27 +51,30 @@ const dataFrame = toDataFrame({
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
exploreId: 'left',
|
||||
loading: LoadingState.NotStarted,
|
||||
width: 800,
|
||||
onCellFilterAdded: jest.fn(),
|
||||
tableResult: [dataFrame],
|
||||
splitOpenFn: () => {},
|
||||
range: getDefaultTimeRange(),
|
||||
timeZone: InternalTimeZones.utc,
|
||||
resultsStyle: TABLE_RESULTS_STYLE.raw,
|
||||
showRawPrometheus: false,
|
||||
};
|
||||
|
||||
describe('RawPrometheusContainer', () => {
|
||||
describe('PrometheusQueryResultsContainer', () => {
|
||||
beforeAll(() => {
|
||||
getTemplateSrv();
|
||||
});
|
||||
|
||||
it('should render component for prometheus', () => {
|
||||
render(<RawPrometheusContainer {...defaultProps} showRawPrometheus={true} />);
|
||||
it('should render table with data and toggle when showRawPrometheus is true', async () => {
|
||||
render(<PrometheusQueryResultsContainer {...defaultProps} showRawPrometheus={true} />);
|
||||
|
||||
// Wait for lazy-loaded component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole('table').length).toBe(1);
|
||||
});
|
||||
|
||||
// Toggle should be visible
|
||||
expect(screen.queryAllByRole('radio').length).toBeGreaterThan(0);
|
||||
|
||||
expect(screen.queryAllByRole('table').length).toBe(1);
|
||||
fireEvent.click(getTableToggle());
|
||||
|
||||
expect(getTable()).toBeInTheDocument();
|
||||
@@ -85,4 +87,25 @@ describe('RawPrometheusContainer', () => {
|
||||
{ time: '2021-01-01 02:00:00', text: 'test_string_4' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render table without toggle when showRawPrometheus is false', async () => {
|
||||
render(<PrometheusQueryResultsContainer {...defaultProps} showRawPrometheus={false} />);
|
||||
|
||||
// Wait for lazy-loaded component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole('table').length).toBe(1);
|
||||
});
|
||||
|
||||
// Toggle should NOT be visible
|
||||
expect(screen.queryAllByRole('radio').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should render empty state when no data', async () => {
|
||||
render(<PrometheusQueryResultsContainer {...defaultProps} tableResult={[]} showRawPrometheus={true} />);
|
||||
|
||||
// Wait for lazy-loaded component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('0 series returned')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { lazy, Suspense, useMemo } from 'react';
|
||||
|
||||
import { applyFieldOverrides, PrometheusQueryResultsV1Props } from '@grafana/data';
|
||||
import { config, getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
const RawPrometheusContainerPureLazy = lazy(() =>
|
||||
import('./RawPrometheusContainerPure').then((m) => ({ default: m.RawPrometheusContainerPure }))
|
||||
);
|
||||
|
||||
/**
|
||||
* EXPOSED COMPONENT (stable): grafana/prometheus-query-results/v1
|
||||
*
|
||||
* This component is exposed to plugins via the Plugin Extensions system.
|
||||
* Treat its props and user-visible behavior as a stable contract. Do not make
|
||||
* breaking changes in-place. If you need to change the API or behavior in a
|
||||
* breaking way, create a new versioned component (e.g. PrometheusQueryResultsV2)
|
||||
* and register it under a new ID: "grafana/prometheus-query-results/v2".
|
||||
*
|
||||
* Displays Prometheus query results with Table/Raw toggle.
|
||||
* Pass raw DataFrames - processing (applyFieldOverrides) is handled internally.
|
||||
*
|
||||
* Example usage in a plugin:
|
||||
* ```typescript
|
||||
* import { usePluginComponent } from '@grafana/runtime';
|
||||
* import { PluginExtensionExposedComponents } from '@grafana/data';
|
||||
*
|
||||
* const { component: PrometheusQueryResults } = usePluginComponent(
|
||||
* PluginExtensionExposedComponents.PrometheusQueryResultsV1
|
||||
* );
|
||||
*
|
||||
* // Render - just pass raw data
|
||||
* <PrometheusQueryResults tableResult={rawDataFrames} width={800} timeZone="browser" />
|
||||
* ```
|
||||
*/
|
||||
export const PrometheusQueryResultsContainer = (props: PrometheusQueryResultsV1Props) => {
|
||||
const width = props.width ?? 800;
|
||||
const timeZone = props.timeZone ?? 'browser';
|
||||
|
||||
// Memoize cloneDeep + applyFieldOverrides to avoid expensive operations on every render
|
||||
// cloneDeep is needed to avoid mutating frozen props from plugin extension system
|
||||
const processedData = useMemo(() => {
|
||||
const tableResult = props.tableResult ?? [];
|
||||
const cloned = cloneDeep(tableResult);
|
||||
if (cloned?.length) {
|
||||
return applyFieldOverrides({
|
||||
data: cloned,
|
||||
timeZone,
|
||||
theme: config.theme2,
|
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
fieldConfig: { defaults: {}, overrides: [] },
|
||||
dataLinkPostProcessor: props.dataLinkPostProcessor,
|
||||
});
|
||||
}
|
||||
return cloned;
|
||||
}, [props.tableResult, timeZone, props.dataLinkPostProcessor]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<RawPrometheusContainerPureLazy
|
||||
tableResult={processedData}
|
||||
width={width}
|
||||
loading={props.loading}
|
||||
ariaLabel={props.ariaLabel}
|
||||
showRawPrometheus={props.showRawPrometheus}
|
||||
onCellFilterAdded={props.onCellFilterAdded}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +1,31 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { applyFieldOverrides, DataFrame, SelectableValue, SplitOpen } from '@grafana/data';
|
||||
import { getTemplateSrv, reportInteraction } from '@grafana/runtime';
|
||||
import { DataFrame, SplitOpen } from '@grafana/data';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { RadioButtonGroup, Table, AdHocFilterItem, PanelChrome } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { ExploreItemState, TABLE_RESULTS_STYLE, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore';
|
||||
import { AdHocFilterItem } from '@grafana/ui';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { MetaInfoText } from '../MetaInfoText';
|
||||
import RawListContainer from '../PrometheusListView/RawListContainer';
|
||||
import { exploreDataLinkPostProcessorFactory } from '../utils/links';
|
||||
|
||||
interface RawPrometheusContainerProps {
|
||||
import { PrometheusQueryResultsContainer } from './PrometheusQueryResultsContainer';
|
||||
|
||||
// ============================================================================
|
||||
// Redux-connected Component - Used by Explore
|
||||
// ============================================================================
|
||||
|
||||
interface ExploreRawPrometheusContainerProps {
|
||||
ariaLabel?: string;
|
||||
exploreId: string;
|
||||
width: number;
|
||||
timeZone: TimeZone;
|
||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||
showRawPrometheus?: boolean;
|
||||
splitOpenFn: SplitOpen;
|
||||
splitOpenFn?: SplitOpen;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId }: RawPrometheusContainerProps) {
|
||||
function mapStateToProps(state: StoreState, { exploreId }: ExploreRawPrometheusContainerProps) {
|
||||
const explore = state.explore;
|
||||
const item: ExploreItemState = explore.panes[exploreId]!;
|
||||
const { rawPrometheusResult, range, queryResponse } = item;
|
||||
@@ -37,121 +37,47 @@ function mapStateToProps(state: StoreState, { exploreId }: RawPrometheusContaine
|
||||
|
||||
const connector = connect(mapStateToProps, {});
|
||||
|
||||
type Props = RawPrometheusContainerProps & ConnectedProps<typeof connector>;
|
||||
type ExploreProps = ExploreRawPrometheusContainerProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export const RawPrometheusContainer = memo(
|
||||
/**
|
||||
* Redux-connected wrapper for Explore.
|
||||
* Gets data from Redux and passes to PrometheusQueryResultsContainer for processing and display.
|
||||
*/
|
||||
const ExploreRawPrometheusContainer = memo(
|
||||
({
|
||||
loading,
|
||||
onCellFilterAdded,
|
||||
tableResult,
|
||||
width,
|
||||
splitOpenFn,
|
||||
range,
|
||||
ariaLabel,
|
||||
timeZone,
|
||||
showRawPrometheus,
|
||||
}: Props) => {
|
||||
// If resultsStyle is undefined we won't render the toggle, and the default table will be rendered
|
||||
const [resultsStyle, setResultsStyle] = useState<TableResultsStyle | undefined>(
|
||||
showRawPrometheus ? TABLE_RESULTS_STYLE.raw : undefined
|
||||
range,
|
||||
splitOpenFn,
|
||||
}: ExploreProps) => {
|
||||
const dataLinkPostProcessor = useMemo(
|
||||
() => exploreDataLinkPostProcessorFactory(splitOpenFn, range),
|
||||
[splitOpenFn, range]
|
||||
);
|
||||
|
||||
const onChangeResultsStyle = (newResultsStyle: TableResultsStyle) => {
|
||||
setResultsStyle(newResultsStyle);
|
||||
};
|
||||
|
||||
const getTableHeight = () => {
|
||||
if (!tableResult || tableResult.length === 0) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
// tries to estimate table height
|
||||
return Math.max(Math.min(600, tableResult[0].length * 35) + 35);
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
const spacing = css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flex: '1',
|
||||
});
|
||||
const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<TableResultsStyle>> = TABLE_RESULTS_STYLES.map((style) => ({
|
||||
value: style,
|
||||
// capital-case it and switch `_` to ` `
|
||||
label: style[0].toUpperCase() + style.slice(1).replace(/_/, ' '),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={spacing}>
|
||||
<RadioButtonGroup
|
||||
onClick={() => {
|
||||
const props = {
|
||||
state: resultsStyle === TABLE_RESULTS_STYLE.table ? TABLE_RESULTS_STYLE.raw : TABLE_RESULTS_STYLE.table,
|
||||
};
|
||||
reportInteraction('grafana_explore_prometheus_instant_query_ui_toggle_clicked', props);
|
||||
}}
|
||||
size="sm"
|
||||
options={ALL_GRAPH_STYLE_OPTIONS}
|
||||
value={resultsStyle}
|
||||
onChange={onChangeResultsStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const height = getTableHeight();
|
||||
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||
|
||||
let dataFrames = tableResult;
|
||||
|
||||
const dataLinkPostProcessor = exploreDataLinkPostProcessorFactory(splitOpenFn, range);
|
||||
|
||||
if (dataFrames?.length) {
|
||||
dataFrames = applyFieldOverrides({
|
||||
data: dataFrames,
|
||||
timeZone,
|
||||
theme: config.theme2,
|
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
dataLinkPostProcessor,
|
||||
});
|
||||
}
|
||||
|
||||
const frames = dataFrames?.filter(
|
||||
(frame: DataFrame | undefined): frame is DataFrame => !!frame && frame.length !== 0
|
||||
);
|
||||
|
||||
const title = resultsStyle === TABLE_RESULTS_STYLE.raw ? 'Raw' : 'Table';
|
||||
const label = resultsStyle !== undefined ? renderLabel() : 'Table';
|
||||
|
||||
// Render table as default if resultsStyle is not set.
|
||||
const renderTable = !resultsStyle || resultsStyle === TABLE_RESULTS_STYLE.table;
|
||||
|
||||
return (
|
||||
<PanelChrome title={title} actions={label} loadingState={loading}>
|
||||
{frames?.length && (
|
||||
<>
|
||||
{renderTable && (
|
||||
<Table
|
||||
ariaLabel={ariaLabel}
|
||||
data={frames[0]}
|
||||
width={tableWidth}
|
||||
height={height}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
/>
|
||||
)}
|
||||
{resultsStyle === TABLE_RESULTS_STYLE.raw && <RawListContainer tableResult={frames[0]} />}
|
||||
</>
|
||||
)}
|
||||
{!frames?.length && <MetaInfoText metaItems={[{ value: '0 series returned' }]} />}
|
||||
</PanelChrome>
|
||||
<PrometheusQueryResultsContainer
|
||||
tableResult={tableResult}
|
||||
width={width}
|
||||
timeZone={timeZone}
|
||||
loading={loading}
|
||||
ariaLabel={ariaLabel}
|
||||
showRawPrometheus={showRawPrometheus}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
dataLinkPostProcessor={dataLinkPostProcessor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RawPrometheusContainer.displayName = 'RawPrometheusContainer';
|
||||
ExploreRawPrometheusContainer.displayName = 'ExploreRawPrometheusContainer';
|
||||
|
||||
export default connector(RawPrometheusContainer);
|
||||
// Keep the old export name for backwards compatibility
|
||||
export const RawPrometheusContainer = ExploreRawPrometheusContainer;
|
||||
|
||||
export default connector(ExploreRawPrometheusContainer);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, LoadingState, SelectableValue } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { RadioButtonGroup, Table, AdHocFilterItem, PanelChrome, useStyles2 } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
import { TABLE_RESULTS_STYLE, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore';
|
||||
|
||||
import { MetaInfoText } from '../MetaInfoText';
|
||||
import RawListContainer from '../PrometheusListView/RawListContainer';
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2) => ({
|
||||
spacing: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flex: '1',
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Props for the pure RawPrometheusContainer component.
|
||||
* This component expects pre-processed DataFrames (caller should apply applyFieldOverrides).
|
||||
*/
|
||||
export interface RawPrometheusContainerPureProps {
|
||||
/** Pre-processed DataFrames to display */
|
||||
tableResult: DataFrame[];
|
||||
/** Width of the container in pixels */
|
||||
width: number;
|
||||
/** Loading state for panel chrome indicator */
|
||||
loading?: LoadingState;
|
||||
/** Aria label for accessibility */
|
||||
ariaLabel?: string;
|
||||
/** Start in Raw view instead of Table view. When true, shows toggle. When false/undefined, shows table only. */
|
||||
showRawPrometheus?: boolean;
|
||||
/** Callback when user adds a cell filter */
|
||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure component for displaying Prometheus query results with Table/Raw toggle.
|
||||
* This component does NOT connect to Redux and expects pre-processed data.
|
||||
*/
|
||||
export const RawPrometheusContainerPure = memo(
|
||||
({
|
||||
loading,
|
||||
onCellFilterAdded,
|
||||
tableResult,
|
||||
width,
|
||||
ariaLabel,
|
||||
showRawPrometheus,
|
||||
}: RawPrometheusContainerPureProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// If resultsStyle is undefined we won't render the toggle, and the default table will be rendered
|
||||
const [resultsStyle, setResultsStyle] = useState<TableResultsStyle | undefined>(
|
||||
showRawPrometheus ? TABLE_RESULTS_STYLE.raw : undefined
|
||||
);
|
||||
|
||||
const onChangeResultsStyle = (newResultsStyle: TableResultsStyle) => {
|
||||
setResultsStyle(newResultsStyle);
|
||||
};
|
||||
|
||||
const getTableHeight = () => {
|
||||
if (!tableResult || tableResult.length === 0) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
// tries to estimate table height
|
||||
return Math.max(Math.min(600, tableResult[0].length * 35) + 35);
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<TableResultsStyle>> = TABLE_RESULTS_STYLES.map((style) => ({
|
||||
value: style,
|
||||
// capital-case it and switch `_` to ` `
|
||||
label: style[0].toUpperCase() + style.slice(1).replace(/_/, ' '),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={styles.spacing}>
|
||||
<RadioButtonGroup
|
||||
onClick={() => {
|
||||
const props = {
|
||||
state: resultsStyle === TABLE_RESULTS_STYLE.table ? TABLE_RESULTS_STYLE.raw : TABLE_RESULTS_STYLE.table,
|
||||
};
|
||||
reportInteraction('grafana_explore_prometheus_instant_query_ui_toggle_clicked', props);
|
||||
}}
|
||||
size="sm"
|
||||
options={ALL_GRAPH_STYLE_OPTIONS}
|
||||
value={resultsStyle}
|
||||
onChange={onChangeResultsStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const height = getTableHeight();
|
||||
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||
|
||||
const frames = tableResult?.filter(
|
||||
(frame: DataFrame | undefined): frame is DataFrame => !!frame && frame.length !== 0
|
||||
);
|
||||
|
||||
const title = resultsStyle === TABLE_RESULTS_STYLE.raw ? 'Raw' : 'Table';
|
||||
const label = resultsStyle !== undefined ? renderLabel() : 'Table';
|
||||
|
||||
// Render table as default if resultsStyle is not set.
|
||||
const renderTable = !resultsStyle || resultsStyle === TABLE_RESULTS_STYLE.table;
|
||||
|
||||
return (
|
||||
<PanelChrome title={title} actions={label} loadingState={loading}>
|
||||
{frames?.length && (
|
||||
<>
|
||||
{renderTable && (
|
||||
<Table
|
||||
ariaLabel={ariaLabel}
|
||||
data={frames[0]}
|
||||
width={tableWidth}
|
||||
height={height}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
/>
|
||||
)}
|
||||
{resultsStyle === TABLE_RESULTS_STYLE.raw && <RawListContainer tableResult={frames[0]} />}
|
||||
</>
|
||||
)}
|
||||
{!frames?.length && <MetaInfoText metaItems={[{ value: '0 series returned' }]} />}
|
||||
</PanelChrome>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RawPrometheusContainerPure.displayName = 'RawPrometheusContainerPure';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PluginExtensionExposedComponents } from '@grafana/data';
|
||||
import CentralAlertHistorySceneExposedComponent from 'app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistorySceneExposedComponent';
|
||||
import { AddToDashboardFormExposedComponent } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardFormExposedComponent';
|
||||
import { PrometheusQueryResultsContainer } from 'app/features/explore/RawPrometheus/PrometheusQueryResultsContainer';
|
||||
|
||||
import { getCoreExtensionConfigurations } from '../getCoreExtensionConfigurations';
|
||||
|
||||
@@ -43,5 +44,11 @@ exposedComponentsRegistry.register({
|
||||
description: 'Add to dashboard form',
|
||||
component: AddToDashboardFormExposedComponent,
|
||||
},
|
||||
{
|
||||
id: PluginExtensionExposedComponents.PrometheusQueryResultsV1,
|
||||
title: 'Prometheus query results',
|
||||
description: 'Display Prometheus query results with Table/Raw toggle',
|
||||
component: PrometheusQueryResultsContainer,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3009,7 +3009,7 @@ __metadata:
|
||||
"@grafana/api-clients": "npm:12.4.0-pre"
|
||||
"@grafana/i18n": "npm:12.4.0-pre"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
"@reduxjs/toolkit": "npm:^2.9.0"
|
||||
"@reduxjs/toolkit": "npm:2.10.1"
|
||||
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||
"@testing-library/react": "npm:^16.3.0"
|
||||
"@testing-library/user-event": "npm:^14.6.1"
|
||||
@@ -3052,7 +3052,7 @@ __metadata:
|
||||
typescript: "npm:5.9.2"
|
||||
peerDependencies:
|
||||
"@grafana/runtime": ">=11.6 <= 12.x"
|
||||
"@reduxjs/toolkit": ^2.8.0
|
||||
"@reduxjs/toolkit": ^2.10.0
|
||||
rxjs: 7.8.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -7294,7 +7294,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:2.10.1, @reduxjs/toolkit@npm:^2.9.0":
|
||||
"@reduxjs/toolkit@npm:2.10.1":
|
||||
version: 2.10.1
|
||||
resolution: "@reduxjs/toolkit@npm:2.10.1"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user