Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a89e155e7 | ||
|
|
5dbbe8164b | ||
|
|
d1064da4cd | ||
|
|
b57b8d4359 | ||
|
|
5219ccddb6 | ||
|
|
c95e3da2d5 | ||
|
|
43d9fbc056 | ||
|
|
7b80c44ac7 | ||
|
|
98f271f345 | ||
|
|
60c4fab063 | ||
|
|
ce8663ac24 | ||
|
|
5dd9a14903 | ||
|
|
68bf19d840 | ||
|
|
220c29de89 | ||
|
|
91ab753368 | ||
|
|
250ca7985f | ||
|
|
b57ed32484 | ||
|
|
d0217588a3 | ||
|
|
ce9ab6a89a | ||
|
|
8c8efd2494 | ||
|
|
69ccfd6bfc | ||
|
|
53aa5e8f7f | ||
|
|
69bf3068b3 | ||
|
|
1263a3d364 | ||
|
|
e4b79e2fc8 | ||
|
|
0d1ec94548 | ||
|
|
23a51ec9c5 | ||
|
|
51dcdd3499 | ||
|
|
880bc23c85 | ||
|
|
6dc604c2ea | ||
|
|
77c500dc01 | ||
|
|
bec4d225b3 | ||
|
|
b91ca14f48 | ||
|
|
2aedbdb76f | ||
|
|
0d7f46c08a | ||
|
|
1b52718c23 | ||
|
|
e61e406440 | ||
|
|
5cb4c311dc | ||
|
|
586410d8b5 | ||
|
|
a0e894c6d8 | ||
|
|
e4796b1de3 | ||
|
|
86a3aae204 | ||
|
|
e0ad4eb7ed | ||
|
|
f0c95a0a10 | ||
|
|
0b46123300 | ||
|
|
81b868ae91 | ||
|
|
2a6a48ac39 | ||
|
|
f581a5a69b | ||
|
|
5ff18c0802 | ||
|
|
0c8c886930 | ||
|
|
ba196958cd | ||
|
|
622c75af6d | ||
|
|
e983aac141 | ||
|
|
c42421e616 | ||
|
|
e4ba1c1a6d | ||
|
|
909ed02218 | ||
|
|
ad3763f04d | ||
|
|
ca6ab973b4 |
@@ -14,6 +14,9 @@ outputs:
|
||||
frontend:
|
||||
description: Whether the frontend or self has changed in any way
|
||||
value: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}
|
||||
frontend-packages:
|
||||
description: Whether any frontend packages have changed
|
||||
value: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}
|
||||
e2e:
|
||||
description: Whether the e2e tests or self have changed in any way
|
||||
value: ${{ steps.changed-files.outputs.e2e_any_changed == 'true' ||
|
||||
@@ -97,6 +100,12 @@ runs:
|
||||
- '.yarn/**'
|
||||
- 'apps/dashboard/pkg/migration/**'
|
||||
- '${{ inputs.self }}'
|
||||
frontend_packages:
|
||||
- '.github/actions/checkout/**'
|
||||
- '.github/actions/change-detection/**'
|
||||
- 'packages/**'
|
||||
- './scripts/validate-npm-packages.sh'
|
||||
- '${{ inputs.self }}'
|
||||
e2e:
|
||||
- 'e2e/**'
|
||||
- 'e2e-playwright/**'
|
||||
@@ -153,6 +162,8 @@ runs:
|
||||
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
|
||||
echo "Frontend: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.frontend_all_changed_files }}"
|
||||
echo "Frontend packages: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.frontend_packages_all_changed_files }}"
|
||||
echo "E2E: ${{ steps.changed-files.outputs.e2e_any_changed || 'true' }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.e2e_all_changed_files }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
|
||||
|
||||
@@ -4,8 +4,8 @@ description: Sets up a node.js environment with presets for the Grafana reposito
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
outputs:
|
||||
changed: ${{ steps.detect-changes.outputs.frontend }}
|
||||
prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }}
|
||||
changed-frontend-packages: ${{ steps.detect-changes.outputs.frontend-packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -42,11 +43,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- run: yarn install --immutable --check-cache
|
||||
- run: yarn run prettier:check
|
||||
- run: yarn run lint
|
||||
@@ -63,11 +61,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Enterprise
|
||||
uses: ./.github/actions/setup-enterprise
|
||||
with:
|
||||
@@ -89,11 +84,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- run: yarn install --immutable --check-cache
|
||||
- run: yarn run typecheck
|
||||
lint-frontend-typecheck-enterprise:
|
||||
@@ -109,11 +101,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Enterprise
|
||||
uses: ./.github/actions/setup-enterprise
|
||||
with:
|
||||
@@ -133,11 +122,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- run: yarn install --immutable --check-cache
|
||||
- name: Generate API clients
|
||||
run: |
|
||||
@@ -164,11 +150,8 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: 'yarn.lock'
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Enterprise
|
||||
uses: ./.github/actions/setup-enterprise
|
||||
with:
|
||||
@@ -187,3 +170,26 @@ jobs:
|
||||
echo "${uncommited_error_message}"
|
||||
exit 1
|
||||
fi
|
||||
lint-frontend-packed-packages:
|
||||
needs: detect-changes
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.changed-frontend-packages == 'true'
|
||||
name: Verify packed frontend packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout build commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build and pack packages
|
||||
run: |
|
||||
yarn run packages:build
|
||||
yarn run packages:pack
|
||||
- name: Validate packages
|
||||
run: ./scripts/validate-npm-packages.sh
|
||||
|
||||
@@ -135,7 +135,7 @@ i18n-extract-enterprise:
|
||||
@echo "Skipping i18n extract for Enterprise: not enabled"
|
||||
else
|
||||
i18n-extract-enterprise:
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
|
||||
endif
|
||||
|
||||
@@ -227,6 +227,10 @@ fix-cue:
|
||||
gen-jsonnet:
|
||||
go generate ./devenv/jsonnet
|
||||
|
||||
.PHONY: gen-themes
|
||||
gen-themes:
|
||||
go generate ./pkg/services/preference
|
||||
|
||||
.PHONY: update-workspace
|
||||
update-workspace: gen-go
|
||||
@echo "updating workspace"
|
||||
@@ -244,6 +248,7 @@ build-go-fast: ## Build all Go binaries without updating workspace.
|
||||
.PHONY: build-backend
|
||||
build-backend: ## Build Grafana backend.
|
||||
@echo "build backend"
|
||||
$(MAKE) gen-themes
|
||||
$(GO) run build.go $(GO_BUILD_FLAGS) build-backend
|
||||
|
||||
.PHONY: build-air
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
|
||||
github.com/grafana/grafana-app-sdk v0.48.7
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7
|
||||
|
||||
@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
|
||||
+206
@@ -852,6 +852,194 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 7,
|
||||
"title": "Single Dashboard DS Query",
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "stat",
|
||||
"spec": {
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-8": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 8,
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 2,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 3,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "C",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "stat",
|
||||
"spec": {
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -914,6 +1102,24 @@
|
||||
"name": "panel-6"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "AutoGridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "AutoGridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+220
@@ -879,6 +879,200 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 7,
|
||||
"title": "Single Dashboard DS Query",
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "stat",
|
||||
"version": "12.1.0-pre",
|
||||
"spec": {
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-8": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 8,
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 2,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 3,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "C",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "stat",
|
||||
"version": "12.1.0-pre",
|
||||
"spec": {
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -973,6 +1167,32 @@
|
||||
"name": "panel-6"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
"width": 8,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 8,
|
||||
"y": 6,
|
||||
"width": 8,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+140
@@ -711,6 +711,146 @@
|
||||
],
|
||||
"title": "Mixed DS WITHOUT REFS",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Single Dashboard DS Query",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "mixed",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 18
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "B",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 3,
|
||||
"refId": "C",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
|
||||
Vendored
+140
@@ -711,6 +711,146 @@
|
||||
],
|
||||
"title": "Mixed DS WITHOUT REFS",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Single Dashboard DS Query",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "mixed",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 18
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "B",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 3,
|
||||
"refId": "C",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
|
||||
Vendored
+212
@@ -879,6 +879,200 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 7,
|
||||
"title": "Single Dashboard DS Query",
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "stat",
|
||||
"version": "12.1.0-pre",
|
||||
"spec": {
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-8": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 8,
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 2,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "datasource",
|
||||
"version": "v0",
|
||||
"datasource": {
|
||||
"name": "-- Dashboard --"
|
||||
},
|
||||
"spec": {
|
||||
"panelId": 3,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"refId": "C",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "stat",
|
||||
"version": "12.1.0-pre",
|
||||
"spec": {
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -941,6 +1135,24 @@
|
||||
"name": "panel-6"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "AutoGridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "AutoGridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+140
@@ -711,6 +711,146 @@
|
||||
],
|
||||
"title": "Mixed DS WITHOUT REFS",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Single Dashboard DS Query",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "mixed",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 6
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "B",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 3,
|
||||
"refId": "C",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
|
||||
+140
@@ -711,6 +711,146 @@
|
||||
],
|
||||
"title": "Mixed DS WITHOUT REFS",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 8,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Single Dashboard DS Query",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "mixed",
|
||||
"uid": "-- Mixed --"
|
||||
},
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 8,
|
||||
"x": 8,
|
||||
"y": 6
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "B",
|
||||
"withTransforms": true
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 3,
|
||||
"refId": "C",
|
||||
"withTransforms": true
|
||||
}
|
||||
],
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
|
||||
Vendored
+214
@@ -852,6 +852,194 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 7,
|
||||
"title": "Single Dashboard DS Query",
|
||||
"description": "Panel with a single -- Dashboard -- datasource query",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "stat",
|
||||
"spec": {
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-8": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 8,
|
||||
"title": "Multiple Dashboard DS Queries",
|
||||
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 1,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 2,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "B",
|
||||
"hidden": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "datasource",
|
||||
"spec": {
|
||||
"panelId": 3,
|
||||
"withTransforms": true
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"refId": "C",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "stat",
|
||||
"spec": {
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -946,6 +1134,32 @@
|
||||
"name": "panel-6"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
"width": 8,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 8,
|
||||
"y": 6,
|
||||
"width": 8,
|
||||
"height": 3,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-8"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1195,16 +1195,36 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer
|
||||
// getPanelDatasource determines the panel-level datasource for V1.
|
||||
// Returns:
|
||||
// - Mixed datasource reference if queries use different datasources
|
||||
// - Mixed datasource reference if multiple queries use Dashboard datasource (they fetch from different panels)
|
||||
// - Dashboard datasource reference if a single query uses Dashboard datasource
|
||||
// - First query's datasource if all queries use the same datasource
|
||||
// - nil if no queries exist
|
||||
// Compares based on V2 input without runtime resolution:
|
||||
// - If query has explicit datasource.uid → use that UID and type
|
||||
// - Else → use query.Kind as type (empty UID)
|
||||
func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
|
||||
const sharedDashboardQuery = "-- Dashboard --"
|
||||
|
||||
if len(queries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count how many queries use Dashboard datasource
|
||||
// Multiple dashboard queries need mixed mode because they fetch from different panels
|
||||
// which may have different underlying datasources
|
||||
dashboardDsQueryCount := 0
|
||||
for _, query := range queries {
|
||||
if query.Spec.Datasource != nil && query.Spec.Datasource.Uid != nil && *query.Spec.Datasource.Uid == sharedDashboardQuery {
|
||||
dashboardDsQueryCount++
|
||||
}
|
||||
}
|
||||
if dashboardDsQueryCount > 1 {
|
||||
return map[string]interface{}{
|
||||
"type": "mixed",
|
||||
"uid": "-- Mixed --",
|
||||
}
|
||||
}
|
||||
|
||||
var firstUID, firstType string
|
||||
var hasFirst bool
|
||||
|
||||
@@ -1239,6 +1259,16 @@ func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[stri
|
||||
}
|
||||
}
|
||||
|
||||
// Handle case when a single query uses Dashboard datasource.
|
||||
// This is needed for the frontend to properly activate and fetch data from source panels.
|
||||
// See DashboardDatasourceBehaviour.tsx for more details.
|
||||
if firstUID == sharedDashboardQuery {
|
||||
return map[string]interface{}{
|
||||
"type": "datasource",
|
||||
"uid": sharedDashboardQuery,
|
||||
}
|
||||
}
|
||||
|
||||
// Not mixed - return the first query's datasource so the panel has a datasource set.
|
||||
// This is required because the frontend's legacy PanelModel.PanelQueryRunner.run uses panel.datasource
|
||||
// to resolve the datasource, and if undefined, it falls back to the default datasource
|
||||
|
||||
+22
-4
@@ -290,7 +290,7 @@
|
||||
],
|
||||
"legend": {
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"placement": "right",
|
||||
"showLegend": true,
|
||||
"values": [
|
||||
"percent"
|
||||
@@ -304,7 +304,7 @@
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showLegend": false,
|
||||
"showLegend": true,
|
||||
"strokeWidth": 1,
|
||||
"text": {}
|
||||
},
|
||||
@@ -323,6 +323,15 @@
|
||||
}
|
||||
],
|
||||
"title": "Percent",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "renameByRegex",
|
||||
"options": {
|
||||
"regex": "^Backend-(.*)$",
|
||||
"renamePattern": "b-$1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
@@ -366,7 +375,7 @@
|
||||
],
|
||||
"legend": {
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"placement": "right",
|
||||
"showLegend": true,
|
||||
"values": [
|
||||
"value"
|
||||
@@ -380,7 +389,7 @@
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showLegend": false,
|
||||
"showLegend": true,
|
||||
"strokeWidth": 1,
|
||||
"text": {}
|
||||
},
|
||||
@@ -399,6 +408,15 @@
|
||||
}
|
||||
],
|
||||
"title": "Value",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "renameByRegex",
|
||||
"options": {
|
||||
"regex": "(.*)",
|
||||
"renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ require (
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
|
||||
+2
-2
@@ -215,8 +215,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
|
||||
@@ -32,7 +32,7 @@ type ConnectionSecure struct {
|
||||
|
||||
// Token is the reference of the token used to act as the Connection.
|
||||
// This value is stored securely and cannot be read back
|
||||
Token common.InlineSecureValue `json:"webhook,omitzero,omitempty"`
|
||||
Token common.InlineSecureValue `json:"token,omitzero,omitempty"`
|
||||
}
|
||||
|
||||
func (v ConnectionSecure) IsZero() bool {
|
||||
|
||||
@@ -320,7 +320,7 @@ func schema_pkg_apis_provisioning_v0alpha1_ConnectionSecure(ref common.Reference
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"),
|
||||
},
|
||||
},
|
||||
"webhook": {
|
||||
"token": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back",
|
||||
Default: map[string]interface{}{},
|
||||
|
||||
-1
@@ -22,7 +22,6 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioni
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ResourceList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,TestResults,Errors
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,WebhookStatus,SubscribedEvents
|
||||
API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionSecure,Token
|
||||
API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,ConnectionSpec,GitHub
|
||||
API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobSpec,PullRequest
|
||||
API rule violation: names_match,github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1,JobStatus,URLs
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Connection --structname MockConnection --inpackage --filename connection_mock.go --with-expecter
|
||||
type Connection interface {
|
||||
// Validate ensures the resource _looks_ correct.
|
||||
// It should be called before trying to upsert a resource into the Kubernetes API server.
|
||||
// This is not an indication that the connection information works, just that they are reasonably configured.
|
||||
Validate(ctx context.Context) error
|
||||
|
||||
// Mutate performs in place mutation of the underneath resource.
|
||||
Mutate(context.Context) error
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package connection
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockConnection is an autogenerated mock type for the Connection type
|
||||
type MockConnection struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockConnection_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockConnection) EXPECT() *MockConnection_Expecter {
|
||||
return &MockConnection_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Mutate provides a mock function with given fields: _a0
|
||||
func (_m *MockConnection) Mutate(_a0 context.Context) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Mutate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_Mutate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mutate'
|
||||
type MockConnection_Mutate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Mutate is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
func (_e *MockConnection_Expecter) Mutate(_a0 interface{}) *MockConnection_Mutate_Call {
|
||||
return &MockConnection_Mutate_Call{Call: _e.mock.On("Mutate", _a0)}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Mutate_Call) Run(run func(_a0 context.Context)) *MockConnection_Mutate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Mutate_Call) Return(_a0 error) *MockConnection_Mutate_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Mutate_Call) RunAndReturn(run func(context.Context) error) *MockConnection_Mutate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Validate provides a mock function with given fields: ctx
|
||||
func (_m *MockConnection) Validate(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Validate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
|
||||
type MockConnection_Validate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Validate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockConnection_Expecter) Validate(ctx interface{}) *MockConnection_Validate_Call {
|
||||
return &MockConnection_Validate_Call{Call: _e.mock.On("Validate", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Validate_Call) Run(run func(ctx context.Context)) *MockConnection_Validate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Validate_Call) Return(_a0 error) *MockConnection_Validate_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Validate_Call) RunAndReturn(run func(context.Context) error) *MockConnection_Validate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockConnection creates a new instance of MockConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockConnection(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockConnection {
|
||||
mock := &MockConnection{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package connection
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockExtra is an autogenerated mock type for the Extra type
|
||||
type MockExtra struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockExtra_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockExtra) EXPECT() *MockExtra_Expecter {
|
||||
return &MockExtra_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Build provides a mock function with given fields: ctx, r
|
||||
func (_m *MockExtra) Build(ctx context.Context, r *v0alpha1.Connection) (Connection, error) {
|
||||
ret := _m.Called(ctx, r)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Build")
|
||||
}
|
||||
|
||||
var r0 Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Connection) (Connection, error)); ok {
|
||||
return rf(ctx, r)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Connection) Connection); ok {
|
||||
r0 = rf(ctx, r)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Connection) error); ok {
|
||||
r1 = rf(ctx, r)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockExtra_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build'
|
||||
type MockExtra_Build_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Build is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Connection
|
||||
func (_e *MockExtra_Expecter) Build(ctx interface{}, r interface{}) *MockExtra_Build_Call {
|
||||
return &MockExtra_Build_Call{Call: _e.mock.On("Build", ctx, r)}
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Build_Call) Run(run func(ctx context.Context, r *v0alpha1.Connection)) *MockExtra_Build_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Connection))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Build_Call) Return(_a0 Connection, _a1 error) *MockExtra_Build_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Build_Call) RunAndReturn(run func(context.Context, *v0alpha1.Connection) (Connection, error)) *MockExtra_Build_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Type provides a mock function with no fields
|
||||
func (_m *MockExtra) Type() v0alpha1.ConnectionType {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Type")
|
||||
}
|
||||
|
||||
var r0 v0alpha1.ConnectionType
|
||||
if rf, ok := ret.Get(0).(func() v0alpha1.ConnectionType); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(v0alpha1.ConnectionType)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockExtra_Type_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Type'
|
||||
type MockExtra_Type_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Type is a helper method to define mock.On call
|
||||
func (_e *MockExtra_Expecter) Type() *MockExtra_Type_Call {
|
||||
return &MockExtra_Type_Call{Call: _e.mock.On("Type")}
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Type_Call) Run(run func()) *MockExtra_Type_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Type_Call) Return(_a0 v0alpha1.ConnectionType) *MockExtra_Type_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockExtra_Type_Call) RunAndReturn(run func() v0alpha1.ConnectionType) *MockExtra_Type_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockExtra creates a new instance of MockExtra. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockExtra(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockExtra {
|
||||
mock := &MockExtra{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
//go:generate mockery --name=Extra --structname=MockExtra --inpackage --filename=extra_mock.go --with-expecter
|
||||
type Extra interface {
|
||||
Type() provisioning.ConnectionType
|
||||
Build(ctx context.Context, r *provisioning.Connection) (Connection, error)
|
||||
}
|
||||
|
||||
//go:generate mockery --name=Factory --structname=MockFactory --inpackage --filename=factory_mock.go --with-expecter
|
||||
type Factory interface {
|
||||
Types() []provisioning.ConnectionType
|
||||
Build(ctx context.Context, r *provisioning.Connection) (Connection, error)
|
||||
}
|
||||
|
||||
type factory struct {
|
||||
extras map[provisioning.ConnectionType]Extra
|
||||
enabled map[provisioning.ConnectionType]struct{}
|
||||
}
|
||||
|
||||
func ProvideFactory(enabled map[provisioning.ConnectionType]struct{}, extras []Extra) (Factory, error) {
|
||||
f := &factory{
|
||||
enabled: enabled,
|
||||
extras: make(map[provisioning.ConnectionType]Extra, len(extras)),
|
||||
}
|
||||
|
||||
for _, e := range extras {
|
||||
if _, exists := f.extras[e.Type()]; exists {
|
||||
return nil, fmt.Errorf("connection type %q is already registered", e.Type())
|
||||
}
|
||||
f.extras[e.Type()] = e
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *factory) Types() []provisioning.ConnectionType {
|
||||
var types []provisioning.ConnectionType
|
||||
for t := range f.enabled {
|
||||
if _, exists := f.extras[t]; exists {
|
||||
types = append(types, t)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(types, func(i, j int) bool {
|
||||
return string(types[i]) < string(types[j])
|
||||
})
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
func (f *factory) Build(ctx context.Context, c *provisioning.Connection) (Connection, error) {
|
||||
for _, e := range f.extras {
|
||||
if e.Type() == c.Spec.Type {
|
||||
if _, enabled := f.enabled[e.Type()]; !enabled {
|
||||
return nil, fmt.Errorf("connection type %q is not enabled", e.Type())
|
||||
}
|
||||
|
||||
return e.Build(ctx, c)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("connection type %q is not supported", c.Spec.Type)
|
||||
}
|
||||
|
||||
var (
|
||||
_ Factory = (*factory)(nil)
|
||||
)
|
||||
@@ -0,0 +1,143 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package connection
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockFactory is an autogenerated mock type for the Factory type
|
||||
type MockFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockFactory_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockFactory) EXPECT() *MockFactory_Expecter {
|
||||
return &MockFactory_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Build provides a mock function with given fields: ctx, r
|
||||
func (_m *MockFactory) Build(ctx context.Context, r *v0alpha1.Connection) (Connection, error) {
|
||||
ret := _m.Called(ctx, r)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Build")
|
||||
}
|
||||
|
||||
var r0 Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Connection) (Connection, error)); ok {
|
||||
return rf(ctx, r)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Connection) Connection); ok {
|
||||
r0 = rf(ctx, r)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Connection) error); ok {
|
||||
r1 = rf(ctx, r)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockFactory_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build'
|
||||
type MockFactory_Build_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Build is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Connection
|
||||
func (_e *MockFactory_Expecter) Build(ctx interface{}, r interface{}) *MockFactory_Build_Call {
|
||||
return &MockFactory_Build_Call{Call: _e.mock.On("Build", ctx, r)}
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Build_Call) Run(run func(ctx context.Context, r *v0alpha1.Connection)) *MockFactory_Build_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Connection))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Build_Call) Return(_a0 Connection, _a1 error) *MockFactory_Build_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Build_Call) RunAndReturn(run func(context.Context, *v0alpha1.Connection) (Connection, error)) *MockFactory_Build_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Types provides a mock function with no fields
|
||||
func (_m *MockFactory) Types() []v0alpha1.ConnectionType {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Types")
|
||||
}
|
||||
|
||||
var r0 []v0alpha1.ConnectionType
|
||||
if rf, ok := ret.Get(0).(func() []v0alpha1.ConnectionType); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]v0alpha1.ConnectionType)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockFactory_Types_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Types'
|
||||
type MockFactory_Types_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Types is a helper method to define mock.On call
|
||||
func (_e *MockFactory_Expecter) Types() *MockFactory_Types_Call {
|
||||
return &MockFactory_Types_Call{Call: _e.mock.On("Types")}
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Types_Call) Run(run func()) *MockFactory_Types_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Types_Call) Return(_a0 []v0alpha1.ConnectionType) *MockFactory_Types_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockFactory_Types_Call) RunAndReturn(run func() []v0alpha1.ConnectionType) *MockFactory_Types_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockFactory creates a new instance of MockFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockFactory(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockFactory {
|
||||
mock := &MockFactory{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestProvideFactory(t *testing.T) {
|
||||
t.Run("should create factory with valid extras", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
provisioning.GitlabConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, factory)
|
||||
})
|
||||
|
||||
t.Run("should create factory with empty extras", func(t *testing.T) {
|
||||
enabled := map[provisioning.ConnectionType]struct{}{}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, factory)
|
||||
})
|
||||
|
||||
t.Run("should create factory with nil enabled map", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
factory, err := ProvideFactory(nil, []Extra{extra1})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, factory)
|
||||
})
|
||||
|
||||
t.Run("should return error when duplicate repository types", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, factory)
|
||||
assert.Contains(t, err.Error(), "connection type \"github\" is already registered")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFactory_Types(t *testing.T) {
|
||||
t.Run("should return only enabled types that have extras", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
provisioning.GitlabConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Len(t, types, 2)
|
||||
assert.Contains(t, types, provisioning.GithubConnectionType)
|
||||
assert.Contains(t, types, provisioning.GitlabConnectionType)
|
||||
})
|
||||
|
||||
t.Run("should return sorted list of types", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
provisioning.GitlabConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Len(t, types, 2)
|
||||
// github should come before gitlab alphabetically
|
||||
assert.Equal(t, provisioning.GithubConnectionType, types[0])
|
||||
assert.Equal(t, provisioning.GitlabConnectionType, types[1])
|
||||
})
|
||||
|
||||
t.Run("should return empty list when no types are enabled", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
|
||||
t.Run("should not return types that are enabled but have no extras", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
provisioning.GitlabConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Len(t, types, 1)
|
||||
assert.Contains(t, types, provisioning.GithubConnectionType)
|
||||
assert.NotContains(t, types, provisioning.GitlabConnectionType)
|
||||
})
|
||||
|
||||
t.Run("should not return types that have extras but are not enabled", func(t *testing.T) {
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Len(t, types, 1)
|
||||
assert.Contains(t, types, provisioning.GithubConnectionType)
|
||||
assert.NotContains(t, types, provisioning.GitlabConnectionType)
|
||||
})
|
||||
|
||||
t.Run("should return empty list when no extras are provided", func(t *testing.T) {
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{})
|
||||
require.NoError(t, err)
|
||||
|
||||
types := factory.Types()
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFactory_Build(t *testing.T) {
|
||||
t.Run("should successfully build connection when type is enabled and has extra", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
},
|
||||
}
|
||||
|
||||
mockConnection := NewMockConnection(t)
|
||||
extra := NewMockExtra(t)
|
||||
extra.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
extra.EXPECT().Build(ctx, conn).Return(mockConnection, nil)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := factory.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, mockConnection, result)
|
||||
})
|
||||
|
||||
t.Run("should return error when type is not enabled", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
},
|
||||
}
|
||||
|
||||
extra := NewMockExtra(t)
|
||||
extra.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := factory.Build(ctx, conn)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "connection type \"gitlab\" is not enabled")
|
||||
})
|
||||
|
||||
t.Run("should return error when type is not supported", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
},
|
||||
}
|
||||
|
||||
extra := NewMockExtra(t)
|
||||
extra.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := factory.Build(ctx, conn)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "connection type \"gitlab\" is not supported")
|
||||
})
|
||||
|
||||
t.Run("should pass through errors from extra.Build()", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
},
|
||||
}
|
||||
|
||||
expectedErr := errors.New("build error")
|
||||
extra := NewMockExtra(t)
|
||||
extra.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
extra.EXPECT().Build(ctx, conn).Return(nil, expectedErr)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := factory.Build(ctx, conn)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("should build with multiple extras registered", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
},
|
||||
}
|
||||
|
||||
mockConnection := NewMockConnection(t)
|
||||
|
||||
extra1 := NewMockExtra(t)
|
||||
extra1.EXPECT().Type().Return(provisioning.GithubConnectionType)
|
||||
|
||||
extra2 := NewMockExtra(t)
|
||||
extra2.EXPECT().Type().Return(provisioning.GitlabConnectionType)
|
||||
extra2.EXPECT().Build(ctx, conn).Return(mockConnection, nil)
|
||||
|
||||
enabled := map[provisioning.ConnectionType]struct{}{
|
||||
provisioning.GithubConnectionType: {},
|
||||
provisioning.GitlabConnectionType: {},
|
||||
}
|
||||
|
||||
factory, err := ProvideFactory(enabled, []Extra{extra1, extra2})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := factory.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, mockConnection, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
// API errors that we need to convey after parsing real GH errors (or faking them).
|
||||
var (
|
||||
//lint:ignore ST1005 this is not punctuation
|
||||
ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable")
|
||||
)
|
||||
|
||||
//go:generate mockery --name Client --structname MockClient --inpackage --filename client_mock.go --with-expecter
|
||||
type Client interface {
|
||||
// Apps and installations
|
||||
GetApp(ctx context.Context) (App, error)
|
||||
GetAppInstallation(ctx context.Context, installationID string) (AppInstallation, error)
|
||||
}
|
||||
|
||||
// App represents a Github App.
|
||||
type App struct {
|
||||
// ID represents the GH app ID.
|
||||
ID int64
|
||||
// Slug represents the GH app slug.
|
||||
Slug string
|
||||
// Owner represents the GH account/org owning the app
|
||||
Owner string
|
||||
}
|
||||
|
||||
// AppInstallation represents a Github App Installation.
|
||||
type AppInstallation struct {
|
||||
// ID represents the GH installation ID.
|
||||
ID int64
|
||||
// Whether the installation is enabled or not.
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type githubClient struct {
|
||||
gh *github.Client
|
||||
}
|
||||
|
||||
func NewClient(client *github.Client) Client {
|
||||
return &githubClient{client}
|
||||
}
|
||||
|
||||
// GetApp gets the app by using the given token.
|
||||
func (r *githubClient) GetApp(ctx context.Context) (App, error) {
|
||||
app, _, err := r.gh.Apps.Get(ctx, "")
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return App{}, ErrServiceUnavailable
|
||||
}
|
||||
return App{}, err
|
||||
}
|
||||
|
||||
// TODO(ferruvich): do we need any other info?
|
||||
return App{
|
||||
ID: app.GetID(),
|
||||
Slug: app.GetSlug(),
|
||||
Owner: app.GetOwner().GetLogin(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAppInstallation gets the installation of the app related to the given token.
|
||||
func (r *githubClient) GetAppInstallation(ctx context.Context, installationID string) (AppInstallation, error) {
|
||||
id, err := strconv.Atoi(installationID)
|
||||
if err != nil {
|
||||
return AppInstallation{}, fmt.Errorf("invalid installation ID: %s", installationID)
|
||||
}
|
||||
|
||||
installation, _, err := r.gh.Apps.GetInstallation(ctx, int64(id))
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return AppInstallation{}, ErrServiceUnavailable
|
||||
}
|
||||
return AppInstallation{}, err
|
||||
}
|
||||
|
||||
// TODO(ferruvich): do we need any other info?
|
||||
return AppInstallation{
|
||||
ID: installation.GetID(),
|
||||
Enabled: installation.GetSuspendedAt().IsZero(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockClient is an autogenerated mock type for the Client type
|
||||
type MockClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockClient) EXPECT() *MockClient_Expecter {
|
||||
return &MockClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetApp provides a mock function with given fields: ctx
|
||||
func (_m *MockClient) GetApp(ctx context.Context) (App, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetApp")
|
||||
}
|
||||
|
||||
var r0 App
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) (App, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) App); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Get(0).(App)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockClient_GetApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApp'
|
||||
type MockClient_GetApp_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetApp is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockClient_Expecter) GetApp(ctx interface{}) *MockClient_GetApp_Call {
|
||||
return &MockClient_GetApp_Call{Call: _e.mock.On("GetApp", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetApp_Call) Run(run func(ctx context.Context)) *MockClient_GetApp_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetApp_Call) Return(_a0 App, _a1 error) *MockClient_GetApp_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetApp_Call) RunAndReturn(run func(context.Context) (App, error)) *MockClient_GetApp_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetAppInstallation provides a mock function with given fields: ctx, installationID
|
||||
func (_m *MockClient) GetAppInstallation(ctx context.Context, installationID string) (AppInstallation, error) {
|
||||
ret := _m.Called(ctx, installationID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAppInstallation")
|
||||
}
|
||||
|
||||
var r0 AppInstallation
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (AppInstallation, error)); ok {
|
||||
return rf(ctx, installationID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) AppInstallation); ok {
|
||||
r0 = rf(ctx, installationID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(AppInstallation)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, installationID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockClient_GetAppInstallation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAppInstallation'
|
||||
type MockClient_GetAppInstallation_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetAppInstallation is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - installationID string
|
||||
func (_e *MockClient_Expecter) GetAppInstallation(ctx interface{}, installationID interface{}) *MockClient_GetAppInstallation_Call {
|
||||
return &MockClient_GetAppInstallation_Call{Call: _e.mock.On("GetAppInstallation", ctx, installationID)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetAppInstallation_Call) Run(run func(ctx context.Context, installationID string)) *MockClient_GetAppInstallation_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetAppInstallation_Call) Return(_a0 AppInstallation, _a1 error) *MockClient_GetAppInstallation_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetAppInstallation_Call) RunAndReturn(run func(context.Context, string) (AppInstallation, error)) *MockClient_GetAppInstallation_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockClient {
|
||||
mock := &MockClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package github_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
conngh "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
|
||||
mockhub "github.com/migueleliasweb/go-github-mock/src/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGithubClient_GetApp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mockHandler *http.Client
|
||||
token string
|
||||
wantApp conngh.App
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "get app successfully",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetApp,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
app := &github.App{
|
||||
ID: github.Ptr(int64(12345)),
|
||||
Slug: github.Ptr("my-test-app"),
|
||||
Owner: &github.User{
|
||||
Login: github.Ptr("grafana"),
|
||||
},
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(app))
|
||||
}),
|
||||
),
|
||||
),
|
||||
token: "test-token",
|
||||
wantApp: conngh.App{
|
||||
ID: 12345,
|
||||
Slug: "my-test-app",
|
||||
Owner: "grafana",
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "service unavailable",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetApp,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
Message: "Service unavailable",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
token: "test-token",
|
||||
wantApp: conngh.App{},
|
||||
wantErr: conngh.ErrServiceUnavailable,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetApp,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
Message: "Internal server error",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
token: "test-token",
|
||||
wantApp: conngh.App{},
|
||||
wantErr: &github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
Message: "Internal server error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unauthorized error",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetApp,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
Message: "Bad credentials",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
token: "invalid-token",
|
||||
wantApp: conngh.App{},
|
||||
wantErr: &github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
Message: "Bad credentials",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a mock client
|
||||
ghClient := github.NewClient(tt.mockHandler)
|
||||
client := conngh.NewClient(ghClient)
|
||||
|
||||
// Call the method being tested
|
||||
app, err := client.GetApp(context.Background())
|
||||
|
||||
// Check the error
|
||||
if tt.wantErr != nil {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantApp, app)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantApp, app)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGithubClient_GetAppInstallation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mockHandler *http.Client
|
||||
appToken string
|
||||
installationID string
|
||||
wantInstallation conngh.AppInstallation
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "get disabled app installation successfully",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetAppInstallationsByInstallationId,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
installation := &github.Installation{
|
||||
ID: github.Ptr(int64(67890)),
|
||||
SuspendedAt: github.Ptr(github.Timestamp{Time: time.Now()}),
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(installation))
|
||||
}),
|
||||
),
|
||||
),
|
||||
appToken: "test-app-token",
|
||||
installationID: "67890",
|
||||
wantInstallation: conngh.AppInstallation{
|
||||
ID: 67890,
|
||||
Enabled: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get enabled app installation successfully",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetAppInstallationsByInstallationId,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
installation := &github.Installation{
|
||||
ID: github.Ptr(int64(67890)),
|
||||
SuspendedAt: nil,
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(installation))
|
||||
}),
|
||||
),
|
||||
),
|
||||
appToken: "test-app-token",
|
||||
installationID: "67890",
|
||||
wantInstallation: conngh.AppInstallation{
|
||||
ID: 67890,
|
||||
Enabled: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid installation ID",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(),
|
||||
appToken: "test-app-token",
|
||||
installationID: "not-a-number",
|
||||
wantInstallation: conngh.AppInstallation{},
|
||||
wantErr: true,
|
||||
errContains: "invalid installation ID",
|
||||
},
|
||||
{
|
||||
name: "service unavailable",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetAppInstallationsByInstallationId,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
Message: "Service unavailable",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
appToken: "test-app-token",
|
||||
installationID: "67890",
|
||||
wantInstallation: conngh.AppInstallation{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "installation not found",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetAppInstallationsByInstallationId,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
},
|
||||
Message: "Not Found",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
appToken: "test-app-token",
|
||||
installationID: "99999",
|
||||
wantInstallation: conngh.AppInstallation{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
mockHandler: mockhub.NewMockedHTTPClient(
|
||||
mockhub.WithRequestMatchHandler(
|
||||
mockhub.GetAppInstallationsByInstallationId,
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
|
||||
Response: &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
Message: "Internal server error",
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
appToken: "test-app-token",
|
||||
installationID: "67890",
|
||||
wantInstallation: conngh.AppInstallation{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a mock client
|
||||
ghClient := github.NewClient(tt.mockHandler)
|
||||
client := conngh.NewClient(ghClient)
|
||||
|
||||
// Call the method being tested
|
||||
installation, err := client.GetAppInstallation(context.Background(), tt.installationID)
|
||||
|
||||
// Check the error
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Check the result
|
||||
assert.Equal(t, tt.wantInstallation, installation)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/connection"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
//go:generate mockery --name GithubFactory --structname MockGithubFactory --inpackage --filename factory_mock.go --with-expecter
|
||||
type GithubFactory interface {
|
||||
New(ctx context.Context, ghToken common.RawSecureValue) Client
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
obj *provisioning.Connection
|
||||
ghFactory GithubFactory
|
||||
}
|
||||
|
||||
func NewConnection(
|
||||
obj *provisioning.Connection,
|
||||
factory GithubFactory,
|
||||
) Connection {
|
||||
return Connection{
|
||||
obj: obj,
|
||||
ghFactory: factory,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
//TODO(ferruvich): these probably need to be setup in API configuration.
|
||||
githubInstallationURL = "https://github.com/settings/installations"
|
||||
jwtExpirationMinutes = 10 // GitHub Apps JWT tokens expire in 10 minutes maximum
|
||||
)
|
||||
|
||||
// Mutate performs in place mutation of the underneath resource.
|
||||
func (c *Connection) Mutate(_ context.Context) error {
|
||||
// Do nothing in case spec.Github is nil.
|
||||
// If this field is required, we should fail at validation time.
|
||||
if c.obj.Spec.GitHub == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.obj.Spec.URL = fmt.Sprintf("%s/%s", githubInstallationURL, c.obj.Spec.GitHub.InstallationID)
|
||||
|
||||
// Generate JWT token if private key is being provided.
|
||||
// Same as for the spec.Github, if such a field is required, Validation will take care of that.
|
||||
if !c.obj.Secure.PrivateKey.Create.IsZero() {
|
||||
token, err := generateToken(c.obj.Spec.GitHub.AppID, c.obj.Secure.PrivateKey.Create)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate JWT token: %w", err)
|
||||
}
|
||||
|
||||
// Store the generated token
|
||||
c.obj.Secure.Token = common.InlineSecureValue{Create: token}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Token generates and returns the Connection token.
|
||||
func generateToken(appID string, privateKey common.RawSecureValue) (common.RawSecureValue, error) {
|
||||
// Decode base64-encoded private key
|
||||
privateKeyPEM, err := base64.StdEncoding.DecodeString(string(privateKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode base64 private key: %w", err)
|
||||
}
|
||||
|
||||
// Parse the private key
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
// Create the JWT token
|
||||
now := time.Now()
|
||||
claims := jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(jwtExpirationMinutes) * time.Minute)),
|
||||
Issuer: appID,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
signedToken, err := token.SignedString(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT token: %w", err)
|
||||
}
|
||||
|
||||
return common.RawSecureValue(signedToken), nil
|
||||
}
|
||||
|
||||
// Validate ensures the resource _looks_ correct.
|
||||
func (c *Connection) Validate(ctx context.Context) error {
|
||||
list := field.ErrorList{}
|
||||
|
||||
if c.obj.Spec.Type != provisioning.GithubConnectionType {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", "type"), c.obj.Spec.Type, "invalid connection type"))
|
||||
|
||||
// Doesn't make much sense to continue validating a connection which is not a Github one.
|
||||
return toError(c.obj.GetName(), list)
|
||||
}
|
||||
|
||||
if c.obj.Spec.GitHub == nil {
|
||||
list = append(
|
||||
list, field.Required(field.NewPath("spec", "github"), "github info must be specified for GitHub connection"),
|
||||
)
|
||||
|
||||
// Doesn't make much sense to continue validating a connection with no information.
|
||||
return toError(c.obj.GetName(), list)
|
||||
}
|
||||
|
||||
if c.obj.Secure.PrivateKey.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "privateKey"), "privateKey must be specified for GitHub connection"))
|
||||
}
|
||||
if c.obj.Secure.Token.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "token"), "token must be specified for GitHub connection"))
|
||||
}
|
||||
if !c.obj.Secure.ClientSecret.IsZero() {
|
||||
list = append(list, field.Forbidden(field.NewPath("secure", "clientSecret"), "clientSecret is forbidden in GitHub connection"))
|
||||
}
|
||||
|
||||
// Validate GitHub configuration fields
|
||||
if c.obj.Spec.GitHub.AppID == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "appID"), "appID must be specified for GitHub connection"))
|
||||
}
|
||||
if c.obj.Spec.GitHub.InstallationID == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "github", "installationID"), "installationID must be specified for GitHub connection"))
|
||||
}
|
||||
|
||||
// In case we have any error above, we don't go forward with the validation, and return the errors.
|
||||
if len(list) > 0 {
|
||||
return toError(c.obj.GetName(), list)
|
||||
}
|
||||
|
||||
// Validating app content via GH API
|
||||
if err := c.validateAppAndInstallation(ctx); err != nil {
|
||||
list = append(list, err)
|
||||
}
|
||||
|
||||
return toError(c.obj.GetName(), list)
|
||||
}
|
||||
|
||||
// validateAppAndInstallation validates the appID and installationID against the given github token.
|
||||
func (c *Connection) validateAppAndInstallation(ctx context.Context) *field.Error {
|
||||
ghClient := c.ghFactory.New(ctx, c.obj.Secure.Token.Create)
|
||||
|
||||
app, err := ghClient.GetApp(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrServiceUnavailable) {
|
||||
return field.InternalError(field.NewPath("spec", "token"), ErrServiceUnavailable)
|
||||
}
|
||||
return field.Invalid(field.NewPath("spec", "token"), "[REDACTED]", "invalid token")
|
||||
}
|
||||
|
||||
if fmt.Sprintf("%d", app.ID) != c.obj.Spec.GitHub.AppID {
|
||||
return field.Invalid(field.NewPath("spec", "appID"), c.obj.Spec.GitHub.AppID, "appID mismatch")
|
||||
}
|
||||
|
||||
_, err = ghClient.GetAppInstallation(ctx, c.obj.Spec.GitHub.InstallationID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrServiceUnavailable) {
|
||||
return field.InternalError(field.NewPath("spec", "token"), ErrServiceUnavailable)
|
||||
}
|
||||
return field.Invalid(field.NewPath("spec", "installationID"), c.obj.Spec.GitHub.InstallationID, "invalid installation ID")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// toError converts a field.ErrorList to an error, returning nil if the list is empty
|
||||
func toError(name string, list field.ErrorList) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewInvalid(
|
||||
provisioning.ConnectionResourceInfo.GroupVersionKind().GroupKind(),
|
||||
name,
|
||||
list,
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
_ connection.Connection = (*Connection)(nil)
|
||||
)
|
||||
@@ -0,0 +1,434 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
//nolint:gosec // Test RSA private key (generated for testing purposes only)
|
||||
const testPrivateKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAoInVbLY9io2Q/wHvUIXlEHg2Qyvd8eRzBAVEJ92DS6fx9H10
|
||||
06V0VRm78S0MXyo6i+n8ZAbZ0/R+GWpP2Ephxm0Gs2zo+iO2mpB19xQFI4o6ZTOw
|
||||
b2WyjSaa2Vr4oyDkqti6AvfjW4VUAu932e08GkgwmmQSHXj7FX2CMWjgUwTTcuaX
|
||||
65SHNKLNYLUP0HTumLzoZeqDTdoMMpKNdgH9Avr4/8vkVJ0mD6rqvxnw3JHsseNO
|
||||
WdQTxf2aApBNHIIKxWZ2i/ZmjLNey7kltgjEquGiBdJvip3fHhH5XHdkrXcjRtnw
|
||||
OJDnDmi5lQwv5yUBOSkbvbXRv/L/m0YLoD/fbwIDAQABAoIBAFfl//hM8/cnuesV
|
||||
+R1Con/ZAgTXQOdPqPXbmEyniVrkMqMmCdBUOBTcST4s5yg36+RtkeaGpb/ajyyF
|
||||
PAB2AYDucwvMpudGpJWOYTiOOp4R8hU1LvZfXVrRd1lo6NgQi4NLtNUpOtACeVQ+
|
||||
H4Yv0YemXQ47mnuOoRNMK/u3q5NoIdSahWptXBgUno8KklNpUrH3IYWaUxfBzDN3
|
||||
2xsVRTn2SfTSyoDmTDdTgptJONmoK1/sV7UsgWksdFc6XyYhsFAZgOGEJrBABRvF
|
||||
546dyQ0cWxuPyVXpM7CN3tqC5ssvLjElg3LicK1V6gnjpdRnnvX88d1Eh3Uc/9IM
|
||||
OZInT2ECgYEA6W8sQXTWinyEwl8SDKKMbB2ApIghAcFgdRxprZE4WFxjsYNCNL70
|
||||
dnSB7MRuzmxf5W77cV0N7JhH66N8HvY6Xq9olrpQ5dNttR4w8Pyv3wavDe8x7seL
|
||||
5L2Xtbu7ihDr8Dk27MjiBSin3IxhBP5CJS910+pR6LrAWtEuU+FzFfECgYEAsA6y
|
||||
qxHhCMXlTnauXhsnmPd1g61q7chW8kLQFYtHMLlQlgjHTW7irDZ9cPbPYDNjwRLO
|
||||
7KLorcpv2NKe7rqq2ZyCm6hf1b9WnlQjo3dLpNWMu6fhy/smK8MgbRqcWpX+oTKF
|
||||
79mK6hbY7o6eBzsQHBl7Z+LBNuwYmp9qOodPa18CgYEArv6ipKdcNhFGzRfMRiCN
|
||||
OHederp6VACNuP2F05IsNUF9kxOdTEFirnKE++P+VU01TqA2azOhPp6iO+ohIGzi
|
||||
MR06QNSH1OL9OWvasK4dggpWrRGF00VQgDgJRTnpS4WH+lxJ6pRlrAxgWpv6F24s
|
||||
VAgSQr1Ejj2B+hMasdMvHWECgYBJ4uE4yhgXBnZlp4kmFV9Y4wF+cZkekaVrpn6N
|
||||
jBYkbKFVVfnOlWqru3KJpgsB5I9IyAvvY68iwIKQDFSG+/AXw4dMrC0MF3DSoZ0T
|
||||
TU2Br92QI7SvVod+djV1lGVp3ukt3XY4YqPZ+hywgUnw3uiz4j3YK2HLGup4ec6r
|
||||
IX5DIQKBgHRLzvT3zqtlR1Oh0vv098clLwt+pGzXOxzJpxioOa5UqK13xIpFXbcg
|
||||
iWUVh5YXCcuqaICUv4RLIEac5xQitk9Is/9IhP0NJ/81rHniosvdSpCeFXzxTImS
|
||||
B8Uc0WUgheB4+yVKGnYpYaSOgFFI5+1BYUva/wDHLy2pWHz39Usb
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
func TestConnection_Mutate(t *testing.T) {
|
||||
t.Run("should add URL to Github connection", func(t *testing.T) {
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
conn := NewConnection(c, mockFactory)
|
||||
|
||||
require.NoError(t, conn.Mutate(context.Background()))
|
||||
assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL)
|
||||
})
|
||||
|
||||
t.Run("should generate JWT token when private key is provided", func(t *testing.T) {
|
||||
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
|
||||
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue(privateKeyBase64),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
conn := NewConnection(c, mockFactory)
|
||||
|
||||
require.NoError(t, conn.Mutate(context.Background()))
|
||||
assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL)
|
||||
assert.False(t, c.Secure.Token.Create.IsZero(), "JWT token should be generated")
|
||||
})
|
||||
|
||||
t.Run("should do nothing when GitHub config is nil", func(t *testing.T) {
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
Gitlab: &provisioning.GitlabConnectionConfig{
|
||||
ClientID: "clientID",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
conn := NewConnection(c, mockFactory)
|
||||
|
||||
require.NoError(t, conn.Mutate(context.Background()))
|
||||
})
|
||||
|
||||
t.Run("should fail when private key is not base64", func(t *testing.T) {
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("invalid-key"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
conn := NewConnection(c, mockFactory)
|
||||
|
||||
err := conn.Mutate(context.Background())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to generate JWT token")
|
||||
assert.Contains(t, err.Error(), "failed to decode base64 private key")
|
||||
})
|
||||
|
||||
t.Run("should fail when private key is invalid", func(t *testing.T) {
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue(base64.StdEncoding.EncodeToString([]byte("invalid-key"))),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
conn := NewConnection(c, mockFactory)
|
||||
|
||||
err := conn.Mutate(context.Background())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to generate JWT token")
|
||||
assert.Contains(t, err.Error(), "failed to parse private key")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConnection_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
connection *provisioning.Connection
|
||||
setupMock func(*MockGithubFactory)
|
||||
wantErr bool
|
||||
errMsgContains []string
|
||||
}{
|
||||
{
|
||||
name: "invalid type returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: "invalid",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.type"},
|
||||
},
|
||||
{
|
||||
name: "github type without github config returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.github"},
|
||||
},
|
||||
{
|
||||
name: "github type without private key returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"secure.privateKey"},
|
||||
},
|
||||
{
|
||||
name: "github type without token returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"secure.token"},
|
||||
},
|
||||
{
|
||||
name: "github type with client secret returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-client-secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"secure.clientSecret"},
|
||||
},
|
||||
{
|
||||
name: "github type without appID returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-token"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.github.appID"},
|
||||
},
|
||||
{
|
||||
name: "github type without installationID returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "test-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.github.installationID"},
|
||||
},
|
||||
{
|
||||
name: "github type with valid config is valid",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-token"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
setupMock: func(mockFactory *MockGithubFactory) {
|
||||
mockClient := NewMockClient(t)
|
||||
|
||||
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
|
||||
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 123, Slug: "test-app"}, nil)
|
||||
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(AppInstallation{ID: 456}, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "problem getting app returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-token"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.token", "[REDACTED]"},
|
||||
setupMock: func(mockFactory *MockGithubFactory) {
|
||||
mockClient := NewMockClient(t)
|
||||
|
||||
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
|
||||
mockClient.EXPECT().GetApp(mock.Anything).Return(App{}, assert.AnError)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mismatched app ID returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-token"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.appID"},
|
||||
setupMock: func(mockFactory *MockGithubFactory) {
|
||||
mockClient := NewMockClient(t)
|
||||
|
||||
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
|
||||
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 444, Slug: "test-app"}, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "problem when getting installation returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-token"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsgContains: []string{"spec.installationID", "456"},
|
||||
setupMock: func(mockFactory *MockGithubFactory) {
|
||||
mockClient := NewMockClient(t)
|
||||
|
||||
mockFactory.EXPECT().New(mock.Anything, common.RawSecureValue("test-token")).Return(mockClient)
|
||||
mockClient.EXPECT().GetApp(mock.Anything).Return(App{ID: 123, Slug: "test-app"}, nil)
|
||||
mockClient.EXPECT().GetAppInstallation(mock.Anything, "456").Return(AppInstallation{}, assert.AnError)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockFactory := NewMockGithubFactory(t)
|
||||
if tt.setupMock != nil {
|
||||
tt.setupMock(mockFactory)
|
||||
}
|
||||
|
||||
conn := NewConnection(tt.connection, mockFactory)
|
||||
err := conn.Validate(context.Background())
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
for _, msg := range tt.errMsgContains {
|
||||
assert.Contains(t, err.Error(), msg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/connection"
|
||||
)
|
||||
|
||||
type extra struct {
|
||||
factory GithubFactory
|
||||
}
|
||||
|
||||
func (e *extra) Type() provisioning.ConnectionType {
|
||||
return provisioning.GithubConnectionType
|
||||
}
|
||||
|
||||
func (e *extra) Build(ctx context.Context, connection *provisioning.Connection) (connection.Connection, error) {
|
||||
logger := logging.FromContext(ctx)
|
||||
if connection == nil || connection.Spec.GitHub == nil {
|
||||
logger.Error("connection is nil or github info is nil")
|
||||
|
||||
return nil, fmt.Errorf("invalid github connection")
|
||||
}
|
||||
|
||||
c := NewConnection(connection, e.factory)
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func Extra(factory GithubFactory) connection.Extra {
|
||||
return &extra{
|
||||
factory: factory,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package github_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestExtra_Type(t *testing.T) {
|
||||
t.Run("should return GithubConnectionType", func(t *testing.T) {
|
||||
mockFactory := github.NewMockGithubFactory(t)
|
||||
e := github.Extra(mockFactory)
|
||||
result := e.Type()
|
||||
assert.Equal(t, provisioning.GithubConnectionType, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtra_Build(t *testing.T) {
|
||||
t.Run("should successfully build connection", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("test-private-key"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := github.NewMockGithubFactory(t)
|
||||
|
||||
e := github.Extra(mockFactory)
|
||||
|
||||
result, err := e.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("should handle different connection configurations", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "another-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "789",
|
||||
InstallationID: "101112",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "existing-private-key",
|
||||
},
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "existing-token",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := github.NewMockGithubFactory(t)
|
||||
|
||||
e := github.Extra(mockFactory)
|
||||
|
||||
result, err := e.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("should build connection with background context", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := github.NewMockGithubFactory(t)
|
||||
e := github.Extra(mockFactory)
|
||||
result, err := e.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("should always pass empty token to factory.New", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
Token: common.InlineSecureValue{
|
||||
Create: common.NewSecretValue("some-token"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockFactory := github.NewMockGithubFactory(t)
|
||||
e := github.Extra(mockFactory)
|
||||
result, err := e.Build(ctx, conn)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// Factory creates new GitHub clients.
|
||||
// It exists only for the ability to test the code easily.
|
||||
type Factory struct {
|
||||
// Client allows overriding the client to use in the GH client returned. It exists primarily for testing.
|
||||
// FIXME: we should replace in this way. We should add some options pattern for the factory.
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func ProvideFactory() GithubFactory {
|
||||
return &Factory{}
|
||||
}
|
||||
|
||||
func (r *Factory) New(ctx context.Context, ghToken common.RawSecureValue) Client {
|
||||
if r.Client != nil {
|
||||
return NewClient(github.NewClient(r.Client))
|
||||
}
|
||||
|
||||
if !ghToken.IsZero() {
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: string(ghToken)},
|
||||
)
|
||||
tokenClient := oauth2.NewClient(ctx, tokenSrc)
|
||||
return NewClient(github.NewClient(tokenClient))
|
||||
}
|
||||
|
||||
return NewClient(github.NewClient(&http.Client{}))
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockGithubFactory is an autogenerated mock type for the GithubFactory type
|
||||
type MockGithubFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockGithubFactory_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockGithubFactory) EXPECT() *MockGithubFactory_Expecter {
|
||||
return &MockGithubFactory_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// New provides a mock function with given fields: ctx, ghToken
|
||||
func (_m *MockGithubFactory) New(ctx context.Context, ghToken v0alpha1.RawSecureValue) Client {
|
||||
ret := _m.Called(ctx, ghToken)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for New")
|
||||
}
|
||||
|
||||
var r0 Client
|
||||
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.RawSecureValue) Client); ok {
|
||||
r0 = rf(ctx, ghToken)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Client)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockGithubFactory_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New'
|
||||
type MockGithubFactory_New_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// New is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - ghToken v0alpha1.RawSecureValue
|
||||
func (_e *MockGithubFactory_Expecter) New(ctx interface{}, ghToken interface{}) *MockGithubFactory_New_Call {
|
||||
return &MockGithubFactory_New_Call{Call: _e.mock.On("New", ctx, ghToken)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubFactory_New_Call) Run(run func(ctx context.Context, ghToken v0alpha1.RawSecureValue)) *MockGithubFactory_New_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(v0alpha1.RawSecureValue))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubFactory_New_Call) Return(_a0 Client) *MockGithubFactory_New_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubFactory_New_Call) RunAndReturn(run func(context.Context, v0alpha1.RawSecureValue) Client) *MockGithubFactory_New_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockGithubFactory creates a new instance of MockGithubFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockGithubFactory(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockGithubFactory {
|
||||
mock := &MockGithubFactory{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
githubInstallationURL = "https://github.com/settings/installations"
|
||||
)
|
||||
|
||||
func MutateConnection(connection *provisioning.Connection) error {
|
||||
switch connection.Spec.Type {
|
||||
case provisioning.GithubConnectionType:
|
||||
// Do nothing in case spec.Github is nil.
|
||||
// If this field is required, we should fail at validation time.
|
||||
if connection.Spec.GitHub == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
connection.Spec.URL = fmt.Sprintf("%s/%s", githubInstallationURL, connection.Spec.GitHub.InstallationID)
|
||||
return nil
|
||||
default:
|
||||
// TODO: we need to setup the URL for bitbucket and gitlab.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package connection_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/connection"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestMutateConnection(t *testing.T) {
|
||||
t.Run("should add URL to Github connection", func(t *testing.T) {
|
||||
c := &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, connection.MutateConnection(c))
|
||||
assert.Equal(t, "https://github.com/settings/installations/456", c.Spec.URL)
|
||||
})
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func ValidateConnection(connection *provisioning.Connection) error {
|
||||
list := field.ErrorList{}
|
||||
|
||||
if connection.Spec.Type == "" {
|
||||
list = append(list, field.Required(field.NewPath("spec", "type"), "type must be specified"))
|
||||
}
|
||||
|
||||
switch connection.Spec.Type {
|
||||
case provisioning.GithubConnectionType:
|
||||
list = append(list, validateGithubConnection(connection)...)
|
||||
case provisioning.BitbucketConnectionType:
|
||||
list = append(list, validateBitbucketConnection(connection)...)
|
||||
case provisioning.GitlabConnectionType:
|
||||
list = append(list, validateGitlabConnection(connection)...)
|
||||
default:
|
||||
list = append(
|
||||
list, field.NotSupported(
|
||||
field.NewPath("spec", "type"),
|
||||
connection.Spec.Type,
|
||||
[]provisioning.ConnectionType{
|
||||
provisioning.GithubConnectionType,
|
||||
provisioning.BitbucketConnectionType,
|
||||
provisioning.GitlabConnectionType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return toError(connection.GetName(), list)
|
||||
}
|
||||
|
||||
func validateGithubConnection(connection *provisioning.Connection) field.ErrorList {
|
||||
list := field.ErrorList{}
|
||||
|
||||
if connection.Spec.GitHub == nil {
|
||||
list = append(
|
||||
list, field.Required(field.NewPath("spec", "github"), "github info must be specified for GitHub connection"),
|
||||
)
|
||||
}
|
||||
|
||||
if connection.Secure.PrivateKey.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "privateKey"), "privateKey must be specified for GitHub connection"))
|
||||
}
|
||||
if !connection.Secure.ClientSecret.IsZero() {
|
||||
list = append(list, field.Forbidden(field.NewPath("secure", "clientSecret"), "clientSecret is forbidden in GitHub connection"))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func validateBitbucketConnection(connection *provisioning.Connection) field.ErrorList {
|
||||
list := field.ErrorList{}
|
||||
|
||||
if connection.Spec.Bitbucket == nil {
|
||||
list = append(
|
||||
list, field.Required(field.NewPath("spec", "bitbucket"), "bitbucket info must be specified in Bitbucket connection"),
|
||||
)
|
||||
}
|
||||
if connection.Secure.ClientSecret.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "clientSecret"), "clientSecret must be specified for Bitbucket connection"))
|
||||
}
|
||||
if !connection.Secure.PrivateKey.IsZero() {
|
||||
list = append(list, field.Forbidden(field.NewPath("secure", "privateKey"), "privateKey is forbidden in Bitbucket connection"))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func validateGitlabConnection(connection *provisioning.Connection) field.ErrorList {
|
||||
list := field.ErrorList{}
|
||||
|
||||
if connection.Spec.Gitlab == nil {
|
||||
list = append(
|
||||
list, field.Required(field.NewPath("spec", "gitlab"), "gitlab info must be specified in Gitlab connection"),
|
||||
)
|
||||
}
|
||||
if connection.Secure.ClientSecret.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "clientSecret"), "clientSecret must be specified for Gitlab connection"))
|
||||
}
|
||||
if !connection.Secure.PrivateKey.IsZero() {
|
||||
list = append(list, field.Forbidden(field.NewPath("secure", "privateKey"), "privateKey is forbidden in Gitlab connection"))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// toError converts a field.ErrorList to an error, returning nil if the list is empty
|
||||
func toError(name string, list field.ErrorList) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewInvalid(
|
||||
provisioning.ConnectionResourceInfo.GroupVersionKind().GroupKind(),
|
||||
name,
|
||||
list,
|
||||
)
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package connection_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/connection"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestValidateConnection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
connection *provisioning.Connection
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "empty type returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "spec.type",
|
||||
},
|
||||
{
|
||||
name: "invalid type returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: "invalid",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "spec.type",
|
||||
},
|
||||
{
|
||||
name: "github type without github config returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "spec.github",
|
||||
},
|
||||
{
|
||||
name: "github type without private key returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.privateKey",
|
||||
},
|
||||
{
|
||||
name: "github type with client secret returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Name: "test-client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.clientSecret",
|
||||
},
|
||||
{
|
||||
name: "github type with github config is valid",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GithubConnectionType,
|
||||
GitHub: &provisioning.GitHubConnectionConfig{
|
||||
AppID: "123",
|
||||
InstallationID: "456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bitbucket type without bitbucket config returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.BitbucketConnectionType,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "spec.bitbucket",
|
||||
},
|
||||
{
|
||||
name: "bitbucket type without client secret returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.BitbucketConnectionType,
|
||||
Bitbucket: &provisioning.BitbucketConnectionConfig{
|
||||
ClientID: "client-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.clientSecret",
|
||||
},
|
||||
{
|
||||
name: "bitbucket type with private key returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.BitbucketConnectionType,
|
||||
Bitbucket: &provisioning.BitbucketConnectionConfig{
|
||||
ClientID: "client-123",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Name: "test-client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.privateKey",
|
||||
},
|
||||
{
|
||||
name: "bitbucket type with bitbucket config is valid",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.BitbucketConnectionType,
|
||||
Bitbucket: &provisioning.BitbucketConnectionConfig{
|
||||
ClientID: "client-123",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Name: "test-client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "gitlab type without gitlab config returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "spec.gitlab",
|
||||
},
|
||||
{
|
||||
name: "gitlab type without client secret returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
Gitlab: &provisioning.GitlabConnectionConfig{
|
||||
ClientID: "client-456",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.clientSecret",
|
||||
},
|
||||
{
|
||||
name: "gitlab type with private key returns error",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
Gitlab: &provisioning.GitlabConnectionConfig{
|
||||
ClientID: "client-456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
PrivateKey: common.InlineSecureValue{
|
||||
Name: "test-private-key",
|
||||
},
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Name: "test-client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "secure.privateKey",
|
||||
},
|
||||
{
|
||||
name: "gitlab type with gitlab config is valid",
|
||||
connection: &provisioning.Connection{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-connection"},
|
||||
Spec: provisioning.ConnectionSpec{
|
||||
Type: provisioning.GitlabConnectionType,
|
||||
Gitlab: &provisioning.GitlabConnectionConfig{
|
||||
ClientID: "client-456",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.ConnectionSecure{
|
||||
ClientSecret: common.InlineSecureValue{
|
||||
Name: "test-client-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := connection.ValidateConnection(tt.connection)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ import (
|
||||
type ConnectionSecureApplyConfiguration struct {
|
||||
PrivateKey *commonv0alpha1.InlineSecureValue `json:"privateKey,omitempty"`
|
||||
ClientSecret *commonv0alpha1.InlineSecureValue `json:"clientSecret,omitempty"`
|
||||
Token *commonv0alpha1.InlineSecureValue `json:"webhook,omitempty"`
|
||||
Token *commonv0alpha1.InlineSecureValue `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectionSecureApplyConfiguration constructs a declarative configuration of the ConnectionSecure type for use with
|
||||
|
||||
+7
-1
@@ -336,7 +336,7 @@ rudderstack_data_plane_url =
|
||||
rudderstack_sdk_url =
|
||||
|
||||
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
|
||||
rudderstack_v3_sdk_url =
|
||||
rudderstack_v3_sdk_url =
|
||||
|
||||
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
|
||||
rudderstack_config_url =
|
||||
@@ -2079,8 +2079,14 @@ enable =
|
||||
# To enable features by default, set `Expression: "true"` in:
|
||||
# https://github.com/grafana/grafana/blob/main/pkg/services/featuremgmt/registry.go
|
||||
|
||||
# The feature_toggles section supports feature flags of a number of types,
|
||||
# including boolean, string, integer, float, and structured values, following the OpenFeature specification.
|
||||
#
|
||||
# feature1 = true
|
||||
# feature2 = false
|
||||
# feature3 = "foobar"
|
||||
# feature4 = 1.5
|
||||
# feature5 = { "foo": "bar" }
|
||||
|
||||
[feature_toggles.openfeature]
|
||||
# This is EXPERIMENTAL. Please, do not use this section
|
||||
|
||||
+8
-3
@@ -323,7 +323,7 @@
|
||||
;rudderstack_sdk_url =
|
||||
|
||||
# Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1
|
||||
;rudderstack_v3_sdk_url =
|
||||
;rudderstack_v3_sdk_url =
|
||||
|
||||
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
|
||||
;rudderstack_config_url =
|
||||
@@ -1913,7 +1913,7 @@ default_datasource_uid =
|
||||
|
||||
# client_queue_max_size is the maximum size in bytes of the client queue
|
||||
# for Live connections. Defaults to 4MB.
|
||||
;client_queue_max_size =
|
||||
;client_queue_max_size =
|
||||
|
||||
#################################### Grafana Image Renderer Plugin ##########################
|
||||
[plugin.grafana-image-renderer]
|
||||
@@ -1996,9 +1996,14 @@ default_datasource_uid =
|
||||
|
||||
;enable = feature1,feature2
|
||||
|
||||
# The feature_toggles section supports feature flags of a number of types,
|
||||
# including boolean, string, integer, float, and structured values, following the OpenFeature specification.
|
||||
|
||||
;feature1 = true
|
||||
;feature2 = false
|
||||
|
||||
;feature3 = "foobar"
|
||||
;feature4 = 1.5
|
||||
;feature5 = { "foo": "bar" }
|
||||
[date_formats]
|
||||
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"legend": {
|
||||
"values": ["percent"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom"
|
||||
"placement": "right"
|
||||
},
|
||||
"pieType": "pie",
|
||||
"reduceOptions": {
|
||||
@@ -256,7 +256,7 @@
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showLegend": false,
|
||||
"showLegend": true,
|
||||
"strokeWidth": 1,
|
||||
"text": {}
|
||||
},
|
||||
@@ -272,6 +272,15 @@
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Percent",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "renameByRegex",
|
||||
"options": {
|
||||
"regex": "^Backend-(.*)$",
|
||||
"renamePattern": "b-$1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
@@ -311,7 +320,7 @@
|
||||
"legend": {
|
||||
"values": ["value"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom"
|
||||
"placement": "right"
|
||||
},
|
||||
"pieType": "pie",
|
||||
"reduceOptions": {
|
||||
@@ -319,7 +328,7 @@
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showLegend": false,
|
||||
"showLegend": true,
|
||||
"strokeWidth": 1,
|
||||
"text": {}
|
||||
},
|
||||
@@ -335,6 +344,15 @@
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Value",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "renameByRegex",
|
||||
"options": {
|
||||
"regex": "(.*)",
|
||||
"renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,6 +48,23 @@ scopes:
|
||||
operator: equals
|
||||
value: kids
|
||||
|
||||
# This scope appears in multiple places in the tree.
|
||||
# The defaultPath determines which path is shown when this scope is selected
|
||||
# (e.g., from a URL or programmatically), even if another path also links to it.
|
||||
shared-service:
|
||||
title: Shared Service
|
||||
# Path from the root node down to the direct scopeNode.
|
||||
# Node names are hierarchical (parent-child), so use the full names.
|
||||
# This points to: gdev-scopes > production > shared-service-prod
|
||||
defaultPath:
|
||||
- gdev-scopes
|
||||
- gdev-scopes-production
|
||||
- gdev-scopes-production-shared-service-prod
|
||||
filters:
|
||||
- key: service
|
||||
operator: equals
|
||||
value: shared
|
||||
|
||||
tree:
|
||||
gdev-scopes:
|
||||
title: gdev-scopes
|
||||
@@ -68,6 +85,13 @@ tree:
|
||||
nodeType: leaf
|
||||
linkId: app2
|
||||
linkType: scope
|
||||
# This node links to 'shared-service' scope.
|
||||
# The scope's defaultPath points here (production > gdev-scopes).
|
||||
shared-service-prod:
|
||||
title: Shared Service
|
||||
nodeType: leaf
|
||||
linkId: shared-service
|
||||
linkType: scope
|
||||
test-cases:
|
||||
title: Test cases
|
||||
nodeType: container
|
||||
@@ -83,6 +107,15 @@ tree:
|
||||
nodeType: leaf
|
||||
linkId: test-case-2
|
||||
linkType: scope
|
||||
# This node also links to the same 'shared-service' scope.
|
||||
# However, the scope's defaultPath points to the production path,
|
||||
# so selecting this scope will expand the tree to production > shared-service-prod.
|
||||
shared-service-test:
|
||||
title: Shared Service (also in Production)
|
||||
subTitle: defaultPath points to Production
|
||||
nodeType: leaf
|
||||
linkId: shared-service
|
||||
linkType: scope
|
||||
test-case-redirect:
|
||||
title: Test case with redirect
|
||||
nodeType: leaf
|
||||
|
||||
+16
-4
@@ -51,8 +51,9 @@ type Config struct {
|
||||
|
||||
// ScopeConfig is used for YAML parsing - converts to v0alpha1.ScopeSpec
|
||||
type ScopeConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
Filters []ScopeFilterConfig `yaml:"filters"`
|
||||
Title string `yaml:"title"`
|
||||
DefaultPath []string `yaml:"defaultPath,omitempty"`
|
||||
Filters []ScopeFilterConfig `yaml:"filters"`
|
||||
}
|
||||
|
||||
// ScopeFilterConfig is used for YAML parsing - converts to v0alpha1.ScopeFilter
|
||||
@@ -116,9 +117,20 @@ func convertScopeSpec(cfg ScopeConfig) v0alpha1.ScopeSpec {
|
||||
for i, f := range cfg.Filters {
|
||||
filters[i] = convertFilter(f)
|
||||
}
|
||||
|
||||
// Prefix defaultPath elements with the gdev prefix
|
||||
var defaultPath []string
|
||||
if len(cfg.DefaultPath) > 0 {
|
||||
defaultPath = make([]string, len(cfg.DefaultPath))
|
||||
for i, p := range cfg.DefaultPath {
|
||||
defaultPath[i] = prefix + "-" + p
|
||||
}
|
||||
}
|
||||
|
||||
return v0alpha1.ScopeSpec{
|
||||
Title: cfg.Title,
|
||||
Filters: filters,
|
||||
Title: cfg.Title,
|
||||
DefaultPath: defaultPath,
|
||||
Filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Plugin signature verification, also known as _signing_, is a security measure to
|
||||
|
||||
Learn more at [plugin policies](https://grafana.com/legal/plugins/).
|
||||
|
||||
## How does verifiction work?
|
||||
## How does verification work?
|
||||
|
||||
At startup, Grafana verifies the signatures of every plugin in the plugin directory.
|
||||
|
||||
|
||||
+12
-11
@@ -66,17 +66,18 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
|
||||
|
||||
The following list contains app plugins that have fine-grained RBAC support.
|
||||
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| [Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/) | `grafana-synthetic-monitoring-app` | [Configure RBAC for Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/user-and-team-management/) |
|
||||
|
||||
### Revoke fine-grained access from app plugins
|
||||
|
||||
|
||||
@@ -99,12 +99,27 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
|
||||
mssql-troubleshoot:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
|
||||
postgres:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
|
||||
mysql:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/
|
||||
---
|
||||
|
||||
# Microsoft SQL Server (MSSQL) data source
|
||||
|
||||
Grafana ships with built-in support for Microsoft SQL Server (MSSQL).
|
||||
You can query and visualize data from any Microsoft SQL Server 2005 or newer, including the Microsoft Azure SQL Database.
|
||||
You can query and visualize data from any Microsoft SQL Server 2005 or newer, including Microsoft Azure SQL Database.
|
||||
|
||||
Use this data source to create dashboards, explore SQL data, and monitor MSSQL-based workloads in real time.
|
||||
|
||||
@@ -113,10 +128,33 @@ The following documentation helps you get started working with the Microsoft SQL
|
||||
- [Configure the Microsoft SQL Server data source](ref:configure-mssql-data-source)
|
||||
- [Microsoft SQL Server query editor](ref:mssql-query-editor)
|
||||
- [Microsoft SQL Server template variables](ref:mssql-template-variables)
|
||||
- [Troubleshoot Microsoft SQL Server data source issues](ref:mssql-troubleshoot)
|
||||
|
||||
## Get the most out of the data source
|
||||
## Supported versions
|
||||
|
||||
After installing and configuring the Microsoft SQL Server data source, you can:
|
||||
This data source supports the following Microsoft SQL Server versions:
|
||||
|
||||
- Microsoft SQL Server 2005 and newer
|
||||
- Microsoft Azure SQL Database
|
||||
- Azure SQL Managed Instance
|
||||
|
||||
Grafana recommends using the latest available service pack for your SQL Server version for optimal compatibility.
|
||||
|
||||
## Key capabilities
|
||||
|
||||
The Microsoft SQL Server data source supports:
|
||||
|
||||
- **Time series queries:** Visualize metrics over time using the built-in time grouping macros.
|
||||
- **Table queries:** Display query results in table format for any valid SQL query.
|
||||
- **Template variables:** Create dynamic dashboards with variable-driven queries.
|
||||
- **Annotations:** Overlay events from SQL Server on your dashboard graphs.
|
||||
- **Alerting:** Create alerts based on SQL Server query results.
|
||||
- **Stored procedures:** Execute stored procedures and visualize results.
|
||||
- **Macros:** Simplify queries with built-in macros for time filtering and grouping.
|
||||
|
||||
## Additional resources
|
||||
|
||||
After configuring the Microsoft SQL Server data source, you can:
|
||||
|
||||
- Create a wide variety of [visualizations](ref:visualizations)
|
||||
- Configure and use [templates and variables](ref:variables)
|
||||
@@ -124,3 +162,8 @@ After installing and configuring the Microsoft SQL Server data source, you can:
|
||||
- Add [annotations](ref:annotate-visualizations)
|
||||
- Set up [alerting](ref:alerting)
|
||||
- Optimize performance with [query caching](ref:query-caching)
|
||||
|
||||
## Related data sources
|
||||
|
||||
- [PostgreSQL](ref:postgres) - For PostgreSQL databases.
|
||||
- [MySQL](ref:mysql) - For MySQL and MariaDB databases.
|
||||
|
||||
@@ -89,6 +89,26 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
mssql-query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
|
||||
mssql-template-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/template-variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/template-variables/
|
||||
alerting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
|
||||
mssql-troubleshoot:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
|
||||
---
|
||||
|
||||
# Configure the Microsoft SQL Server data source
|
||||
@@ -97,13 +117,28 @@ This document provides instructions for configuring the Microsoft SQL Server dat
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Grafana comes with a built-in MSSQL data source plugin, eliminating the need to install a plugin.
|
||||
Before configuring the Microsoft SQL Server data source, ensure you have the following:
|
||||
|
||||
- You must have the `Organization administrator` role to configure the MSSQL data source. Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system.
|
||||
- **Grafana permissions:** You must have the `Organization administrator` role to configure data sources. Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system.
|
||||
|
||||
- Familiarize yourself with your MSSQL security configuration and gather any necessary security certificates and client keys.
|
||||
- **A running SQL Server instance:** Microsoft SQL Server 2005 or newer, Azure SQL Database, or Azure SQL Managed Instance.
|
||||
|
||||
- Verify that data from MSSQL is being written to your Grafana instance.
|
||||
- **Network access:** Grafana must be able to reach your SQL Server. The default port is `1433`.
|
||||
|
||||
- **Authentication credentials:** Depending on your authentication method, you need one of:
|
||||
- SQL Server login credentials (username and password).
|
||||
- Windows/Kerberos credentials and configuration (not supported in Grafana Cloud).
|
||||
- Azure Entra ID app registration or managed identity.
|
||||
|
||||
- **Security certificates:** If using encrypted connections, gather any necessary TLS/SSL certificates.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Grafana ships with a built-in Microsoft SQL Server data source plugin. No additional installation is required.
|
||||
{{< /admonition >}}
|
||||
|
||||
{{< admonition type="tip" >}}
|
||||
**Grafana Cloud users:** If your SQL Server is in a private network, you can configure [Private data source connect](ref:private-data-source-connect) to establish connectivity.
|
||||
{{< /admonition >}}
|
||||
|
||||
## Add the MSSQL data source
|
||||
|
||||
@@ -382,3 +417,48 @@ datasources:
|
||||
secureJsonData:
|
||||
password: 'Password!'
|
||||
```
|
||||
|
||||
### Configure with Terraform
|
||||
|
||||
You can configure the Microsoft SQL Server data source using [Terraform](https://www.terraform.io/) with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
|
||||
|
||||
For more information about provisioning resources with Terraform, refer to the [Grafana as code using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/) documentation.
|
||||
|
||||
#### Terraform example
|
||||
|
||||
The following example creates a basic Microsoft SQL Server data source:
|
||||
|
||||
```hcl
|
||||
resource "grafana_data_source" "mssql" {
|
||||
name = "MSSQL"
|
||||
type = "mssql"
|
||||
url = "localhost:1433"
|
||||
user = "grafana"
|
||||
|
||||
json_data_encoded = jsonencode({
|
||||
database = "grafana"
|
||||
maxOpenConns = 100
|
||||
maxIdleConns = 100
|
||||
maxIdleConnsAuto = true
|
||||
connMaxLifetime = 14400
|
||||
connectionTimeout = 0
|
||||
encrypt = "false"
|
||||
})
|
||||
|
||||
secure_json_data_encoded = jsonencode({
|
||||
password = "Password!"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For all available configuration options, refer to the [Grafana provider data source resource documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source).
|
||||
|
||||
## Next steps
|
||||
|
||||
After configuring your Microsoft SQL Server data source, you can:
|
||||
|
||||
- [Write queries](ref:mssql-query-editor) using the query editor to explore and visualize your data
|
||||
- [Create template variables](ref:mssql-template-variables) to build dynamic, reusable dashboards
|
||||
- [Add annotations](ref:annotate-visualizations) to overlay SQL Server events on your graphs
|
||||
- [Set up alerting](ref:alerting) to create alert rules based on your SQL Server data
|
||||
- [Troubleshoot issues](ref:mssql-troubleshoot) if you encounter problems with your data source
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
---
|
||||
description: Troubleshoot common problems with the Microsoft SQL Server data source in Grafana
|
||||
keywords:
|
||||
- grafana
|
||||
- MSSQL
|
||||
- Microsoft
|
||||
- SQL
|
||||
- troubleshooting
|
||||
- errors
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Troubleshooting
|
||||
title: Troubleshoot Microsoft SQL Server data source issues
|
||||
weight: 400
|
||||
refs:
|
||||
configure-mssql-data-source:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/configure/
|
||||
mssql-query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
|
||||
private-data-source-connect:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
---
|
||||
|
||||
# Troubleshoot Microsoft SQL Server data source issues
|
||||
|
||||
This document provides solutions to common issues you may encounter when configuring or using the Microsoft SQL Server (MSSQL) data source in Grafana.
|
||||
|
||||
## Connection errors
|
||||
|
||||
These errors occur when Grafana cannot establish or maintain a connection to the Microsoft SQL Server.
|
||||
|
||||
### Unable to connect to the server
|
||||
|
||||
**Error message:** "Unable to open tcp connection" or "dial tcp: connection refused"
|
||||
|
||||
**Cause:** Grafana cannot establish a network connection to the SQL Server.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify that the SQL Server is running and accessible.
|
||||
1. Check that the host and port are correct in the data source configuration. The default SQL Server port is `1433`.
|
||||
1. Ensure there are no firewall rules blocking the connection between Grafana and SQL Server.
|
||||
1. Verify that SQL Server is configured to allow remote connections.
|
||||
1. For Grafana Cloud, ensure you have configured [Private data source connect](ref:private-data-source-connect) if your SQL Server instance is not publicly accessible.
|
||||
|
||||
### Connection timeout
|
||||
|
||||
**Error message:** "Connection timed out" or "I/O timeout"
|
||||
|
||||
**Cause:** The connection to SQL Server timed out before receiving a response.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Check the network latency between Grafana and SQL Server.
|
||||
1. Verify that SQL Server is not overloaded or experiencing performance issues.
|
||||
1. Increase the **Connection timeout** setting in the data source configuration under **Additional settings**.
|
||||
1. Check if any network devices (load balancers, proxies) are timing out the connection.
|
||||
|
||||
### Encryption-related connection failures
|
||||
|
||||
**Error message:** "TLS handshake failed" or "certificate verify failed"
|
||||
|
||||
**Cause:** There is a mismatch between the encryption settings in Grafana and what the SQL Server supports or requires.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. For older versions of SQL Server (2008, 2008R2), set the **Encrypt** option to **Disable** or **False** in the data source configuration.
|
||||
1. Verify that the SQL Server has a valid SSL certificate if encryption is enabled.
|
||||
1. Check that the certificate is trusted by the Grafana server.
|
||||
1. Ensure you're using the latest available service pack for your SQL Server version for optimal compatibility.
|
||||
|
||||
### Named instance connection issues
|
||||
|
||||
**Error message:** "Cannot connect to named instance" or connection fails when using instance name
|
||||
|
||||
**Cause:** Grafana cannot resolve the SQL Server named instance.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Use the format `hostname\instancename` or `hostname\instancename,port` in the **Host** field.
|
||||
1. Verify that the SQL Server Browser service is running on the SQL Server machine.
|
||||
1. If the Browser service is unavailable, specify the port number directly: `hostname,port`.
|
||||
1. Check that UDP port 1434 is open if using the SQL Server Browser service.
|
||||
|
||||
## Authentication errors
|
||||
|
||||
These errors occur when there are issues with authentication credentials or permissions.
|
||||
|
||||
### Login failed for user
|
||||
|
||||
**Error message:** "Login failed for user 'username'" or "Authentication failed"
|
||||
|
||||
**Cause:** The authentication credentials are invalid or the user doesn't have permission to access the database.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify that the username and password are correct.
|
||||
1. Check that the user exists in SQL Server and is enabled.
|
||||
1. Ensure the user has access to the specified database.
|
||||
1. For Windows Authentication, verify that the credentials are in the correct format (`DOMAIN\User`).
|
||||
1. Check that the SQL Server authentication mode allows the type of login you're using (SQL Server Authentication, Windows Authentication, or Mixed Mode).
|
||||
|
||||
### Access denied to database
|
||||
|
||||
**Error message:** "Cannot open database 'dbname' requested by the login"
|
||||
|
||||
**Cause:** The authenticated user doesn't have permission to access the specified database.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify that the database name is correct in the data source configuration.
|
||||
1. Ensure the user is mapped to the database with appropriate permissions.
|
||||
1. Grant at least `SELECT` permission on the required tables:
|
||||
|
||||
```sql
|
||||
USE [your_database]
|
||||
GRANT SELECT ON dbo.YourTable TO [your_user]
|
||||
```
|
||||
|
||||
1. Check that the user doesn't have any conflicting permissions from the public role.
|
||||
|
||||
### Windows Authentication (Kerberos) issues
|
||||
|
||||
**Error message:** "Kerberos authentication failed" or "Cannot initialize Kerberos"
|
||||
|
||||
**Cause:** Kerberos configuration is incorrect or incomplete.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify that the Kerberos configuration file (`krb5.conf`) path is correct in the data source settings.
|
||||
1. For keytab authentication, ensure the keytab file exists and is readable by Grafana.
|
||||
1. Check that the realm and KDC settings are correct in the Kerberos configuration.
|
||||
1. Verify DNS is correctly resolving the KDC servers.
|
||||
1. Ensure the service principal name (SPN) is registered for the SQL Server instance.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Kerberos authentication is not supported in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
### Azure Entra ID authentication errors
|
||||
|
||||
**Error message:** "AADSTS error codes" or "Azure AD authentication failed"
|
||||
|
||||
**Cause:** Azure Entra ID (formerly Azure AD) authentication is misconfigured.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. For **App Registration** authentication:
|
||||
- Verify the tenant ID, client ID, and client secret are correct.
|
||||
- Ensure the app registration has been added as a user in the Azure SQL database.
|
||||
- Check that the client secret hasn't expired.
|
||||
|
||||
1. For **Managed Identity** authentication:
|
||||
- Verify `managed_identity_enabled = true` is set in the Grafana server configuration.
|
||||
- Ensure the managed identity has been added to the Azure SQL database.
|
||||
- Confirm the Azure resource hosting Grafana has managed identity enabled.
|
||||
|
||||
1. For **Current User** authentication:
|
||||
- Ensure `user_identity_enabled = true` is set in the Grafana server configuration.
|
||||
- Verify the app registration is configured to issue both Access Tokens and ID Tokens.
|
||||
- Check that the required API permissions are configured (`user_impersonation` for Azure SQL).
|
||||
|
||||
For detailed Azure authentication configuration, refer to [Configure the Microsoft SQL Server data source](ref:configure-mssql-data-source).
|
||||
|
||||
## Query errors
|
||||
|
||||
These errors occur when there are issues with query syntax or configuration.
|
||||
|
||||
### Time column not found or invalid
|
||||
|
||||
**Error message:** "Could not find time column" or time series visualization shows no data
|
||||
|
||||
**Cause:** The query doesn't return a properly formatted `time` column for time series visualization.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Ensure your query includes a column named `time` when using the **Time series** format.
|
||||
1. Use the `$__time()` macro to rename your date column: `$__time(your_date_column)`.
|
||||
1. Verify the time column is of a valid SQL date/time type (`datetime`, `datetime2`, `date`) or contains Unix epoch values.
|
||||
1. Ensure the result set is sorted by the time column using `ORDER BY`.
|
||||
|
||||
### Macro expansion errors
|
||||
|
||||
**Error message:** "Error parsing query" or macros appear unexpanded in the query
|
||||
|
||||
**Cause:** Grafana macros are being used incorrectly.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify macro syntax: use `$__timeFilter(column)` not `$_timeFilter(column)`.
|
||||
1. Macros don't work inside stored procedures—use explicit date parameters instead.
|
||||
1. Check that the column name passed to macros exists in your table.
|
||||
1. View the expanded query by clicking **Generated SQL** after running the query to debug macro expansion.
|
||||
|
||||
### Timezone and time shift issues
|
||||
|
||||
**Cause:** Time series data appears shifted or doesn't align with expected times.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Store timestamps in UTC in your database to avoid timezone issues.
|
||||
1. Time macros (`$__time`, `$__timeFilter`, etc.) always expand to UTC values.
|
||||
1. If your timestamps are stored in local time, convert them to UTC in your query:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
your_datetime_column AT TIME ZONE 'Your Local Timezone' AT TIME ZONE 'UTC' AS time,
|
||||
value
|
||||
FROM your_table
|
||||
```
|
||||
|
||||
1. Don't pass timezone parameters to time macros—they're not supported.
|
||||
|
||||
### Query returns too many rows
|
||||
|
||||
**Error message:** "Result set too large" or browser becomes unresponsive
|
||||
|
||||
**Cause:** The query returns more data than can be efficiently processed.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Add time filters using `$__timeFilter(column)` to limit data to the dashboard time range.
|
||||
1. Use aggregations (`AVG`, `SUM`, `COUNT`) with `GROUP BY` instead of returning raw rows.
|
||||
1. Add a `TOP` clause to limit results: `SELECT TOP 1000 ...`.
|
||||
1. Use the `$__timeGroup()` macro to aggregate data into time intervals.
|
||||
|
||||
### Stored procedure returns no data
|
||||
|
||||
**Cause:** Stored procedure output isn't being captured correctly.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Ensure the stored procedure uses `SELECT` statements, not just variable assignments.
|
||||
1. Remove `SET NOCOUNT ON` if present, or ensure it's followed by a `SELECT` statement.
|
||||
1. Verify the stored procedure parameters are being passed correctly.
|
||||
1. Test the stored procedure directly in SQL Server Management Studio with the same parameters.
|
||||
|
||||
For more information on using stored procedures, refer to the [query editor documentation](ref:mssql-query-editor).
|
||||
|
||||
## Performance issues
|
||||
|
||||
These issues relate to slow queries or high resource usage.
|
||||
|
||||
### Slow query execution
|
||||
|
||||
**Cause:** Queries take a long time to execute.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Reduce the dashboard time range to limit data volume.
|
||||
1. Add indexes to columns used in `WHERE` clauses and time filters.
|
||||
1. Use aggregations instead of returning individual rows.
|
||||
1. Increase the **Min time interval** setting to reduce the number of data points.
|
||||
1. Review the query execution plan in SQL Server Management Studio to identify bottlenecks.
|
||||
|
||||
### Connection pool exhaustion
|
||||
|
||||
**Error message:** "Too many connections" or "Connection pool exhausted"
|
||||
|
||||
**Cause:** Too many concurrent connections to the database.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Increase the **Max open** connection limit in the data source configuration.
|
||||
1. Enable **Auto max idle** to automatically manage idle connections.
|
||||
1. Reduce the number of panels querying the same data source simultaneously.
|
||||
1. Check for long-running queries that might be holding connections.
|
||||
|
||||
## Other common issues
|
||||
|
||||
The following issues don't produce specific error messages but are commonly encountered.
|
||||
|
||||
### System databases appear in queries
|
||||
|
||||
**Cause:** Queries accidentally access system databases.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. The query editor automatically excludes `tempdb`, `model`, `msdb`, and `master` from the database dropdown.
|
||||
1. Always specify the database in your data source configuration to restrict access.
|
||||
1. Ensure the database user only has permissions on the intended database.
|
||||
|
||||
### Template variable queries fail
|
||||
|
||||
**Cause:** Variable queries return unexpected results or errors.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify the variable query syntax is valid SQL that returns a single column.
|
||||
1. Check that the data source connection is working.
|
||||
1. Ensure the user has permission to access the tables referenced in the variable query.
|
||||
1. Test the query in the query editor before using it as a variable query.
|
||||
|
||||
### Data appears incorrect or misaligned
|
||||
|
||||
**Cause:** Data formatting or type conversion issues.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Use explicit column aliases to ensure consistent naming: `SELECT value AS metric`.
|
||||
1. Verify numeric columns are actually numeric types, not strings.
|
||||
1. Check for `NULL` values that might affect aggregations.
|
||||
1. Use the `FILL` option in `$__timeGroup()` macro to handle missing data points.
|
||||
|
||||
## Get additional help
|
||||
|
||||
If you continue to experience issues after following this troubleshooting guide:
|
||||
|
||||
1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues.
|
||||
1. Review the [Grafana GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
|
||||
1. Enable debug logging in Grafana to capture detailed error information.
|
||||
1. Check SQL Server logs for additional error details.
|
||||
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
|
||||
|
||||
When reporting issues, include:
|
||||
|
||||
- Grafana version
|
||||
- SQL Server version
|
||||
- Error messages (redact sensitive information)
|
||||
- Steps to reproduce
|
||||
- Relevant query examples (redact sensitive data)
|
||||
@@ -231,6 +231,10 @@ JSON Body schema:
|
||||
- **metadata.annotations.grafana.app/folder** - Optional field, the unique identifier of the folder under which the dashboard should be created.
|
||||
- **spec** – The dashboard json.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Custom labels and annotations in the metadata field are supported on some instances, with full support planned for all instances when these APIs reach general availability. If they are not yet supported on your instance, they will be ignored.
|
||||
{{< /admonition >}}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -521,6 +525,10 @@ JSON Body schema:
|
||||
- **metadata.annotations.grafana.app/message** - Optional field, to set a commit message for the version history.
|
||||
- **spec** – The dashboard json.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Custom labels and annotations in the metadata field are supported on some instances, with full support planned for all instances when these APIs reach general availability. If they are not yet supported on your instance, they will be ignored.
|
||||
{{< /admonition >}}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
|
||||
@@ -259,6 +259,10 @@ JSON Body schema:
|
||||
- **metadata.annotations.grafana.app/folder** - Optional field, the unique identifier of the parent folder under which the folder should be created. Requires nested folders to be enabled.
|
||||
- **spec.title** – The title of the folder.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Custom labels and annotations in the metadata field are supported on some instances, with full support planned for all instances when these APIs reach general availability. If they are not yet supported on your instance, they will be ignored.
|
||||
{{< /admonition >}}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -337,6 +341,10 @@ JSON Body schema:
|
||||
- **metadata.annotations.grafana.app/folder** - Optional field, the unique identifier of the parent folder under which the folder should be - update this to move the folder under a different parent folder. Requires nested folders to be enabled.
|
||||
- **spec.title** – The title of the folder.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Custom labels and annotations in the metadata field are supported on some instances, with full support planned for all instances when these APIs reach general availability. If they are not yet supported on your instance, they will be ignored.
|
||||
{{< /admonition >}}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
|
||||
@@ -2836,9 +2836,11 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise](../
|
||||
|
||||
Keys of features to enable, separated by space.
|
||||
|
||||
#### `FEATURE_TOGGLE_NAME = false`
|
||||
#### `FEATURE_NAME = <value>`
|
||||
|
||||
Some feature toggles for stable features are on by default. Use this setting to disable an on-by-default feature toggle with the name FEATURE_TOGGLE_NAME, for example, `exploreMixedDatasource = false`.
|
||||
Use a key-value pair to set feature flag values explicitly, overriding any default values. A few different types are supported, following the OpenFeature specification. See the defaults.ini file for more details.
|
||||
|
||||
For example, to disable an on-by-default feature toggle named `exploreMixedDatasource`, specify `exploreMixedDatasource = false`.
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes |
|
||||
| `tabularNumbers` | Use fixed-width numbers globally in the UI | |
|
||||
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes |
|
||||
| `tempoSearchBackendMigration` | Run search queries through the tempo backend | |
|
||||
| `opentsdbBackendMigration` | Run queries through the data source backend | |
|
||||
|
||||
## Public preview feature toggles
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
import {
|
||||
getAdHocFilterOptionValues,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from './cuj-selectors';
|
||||
import { prepareAPIMocks } from './utils';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
|
||||
test.use({
|
||||
@@ -34,6 +36,11 @@ test.describe(
|
||||
const adHocFilterPills = getAdHocFilterPills(page);
|
||||
const scopesSelectorInput = getScopesSelectorInput(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Apply filtering to a whole dashboard', async () => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
|
||||
@@ -66,6 +66,17 @@ export function getScopesDashboards(page: Page) {
|
||||
return page.locator('[data-testid^="scopes-dashboards-"][role="treeitem"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the first available dashboard in the scopes dashboard list.
|
||||
*/
|
||||
export async function clickFirstScopesDashboard(page: Page) {
|
||||
const dashboards = getScopesDashboards(page);
|
||||
// Wait for at least one dashboard to be visible
|
||||
await expect(dashboards.first()).toBeVisible({ timeout: 10000 });
|
||||
// Click - Playwright will automatically wait for the element to be actionable
|
||||
await dashboards.first().click();
|
||||
}
|
||||
|
||||
export function getScopesDashboardsSearchInput(page: Page) {
|
||||
return page.getByTestId('scopes-dashboards-search');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
import {
|
||||
clickFirstScopesDashboard,
|
||||
getAdHocFilterPills,
|
||||
getGroupByInput,
|
||||
getGroupByValues,
|
||||
@@ -21,6 +23,7 @@ test.use({
|
||||
},
|
||||
});
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
const DASHBOARD_UNDER_TEST_2 = 'cuj-dashboard-2';
|
||||
const NAVIGATE_TO = 'cuj-dashboard-3';
|
||||
@@ -38,6 +41,11 @@ test.describe(
|
||||
const adhocFilterPills = getAdHocFilterPills(page);
|
||||
const groupByValues = getGroupByValues(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Search dashboard', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
@@ -74,7 +82,7 @@ test.describe(
|
||||
|
||||
await expect(markdownContent).toContainText(`now-12h`);
|
||||
|
||||
await scopesDashboards.first().click();
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
await expect(markdownContent).toBeVisible();
|
||||
@@ -117,10 +125,10 @@ test.describe(
|
||||
await groupByVariable.press('Enter');
|
||||
await groupByVariable.press('Escape');
|
||||
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
|
||||
const { getRequests, waitForExpectedRequests } = await trackDashboardReloadRequests(page);
|
||||
await scopesDashboards.first().click();
|
||||
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
await waitForExpectedRequests();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
@@ -158,8 +166,7 @@ test.describe(
|
||||
const oldFilters = `GroupByVar: ${selectedValues}\n\nAdHocVar: ${processedPills}`;
|
||||
await expect(markdownContent).toContainText(oldFilters);
|
||||
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
await scopesDashboards.first().click();
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
const newPillCount = await adhocFilterPills.count();
|
||||
|
||||
@@ -165,9 +165,8 @@ test.describe(
|
||||
|
||||
await refreshBtn.click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
expect(await panelContent.textContent()).not.toBe(panelContents);
|
||||
// Wait for the panel content to change (not just for network to complete)
|
||||
await expect(panelContent).not.toHaveText(panelContents!, { timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('6.Turn off refresh', async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
openScopesSelector,
|
||||
searchScopes,
|
||||
selectScope,
|
||||
setupScopeRoutes,
|
||||
} from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
@@ -36,32 +37,37 @@ test.describe(
|
||||
const scopesSelector = getScopesSelectorInput(page);
|
||||
const recentScopesSelector = getRecentScopesSelector(page);
|
||||
const scopeTreeCheckboxes = getScopeTreeCheckboxes(page);
|
||||
const scopes = testScopes();
|
||||
|
||||
// Set up routes once before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
}
|
||||
|
||||
await test.step('1.View and select any scope', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitle);
|
||||
});
|
||||
@@ -70,28 +76,27 @@ test.describe(
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeTitles: string[] = [];
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]];
|
||||
for (let i = 0; i < selectedScopes.length; i++) {
|
||||
scopeName = await getScopeLeafName(page, i);
|
||||
scopeTitles.push(await getScopeLeafTitle(page, i));
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]); //used only in mocked scopes version
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]);
|
||||
}
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
|
||||
await expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitles.join(' + '));
|
||||
});
|
||||
@@ -102,8 +107,7 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
await recentScopesSelector.click();
|
||||
|
||||
@@ -121,26 +125,25 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const firstLevelScopes = scopes[2].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[2].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []);
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', new RegExp(`^${scopeTitle}`));
|
||||
});
|
||||
@@ -148,17 +151,16 @@ test.describe(
|
||||
await test.step('5.View pre-completed production entity values as I type', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeSearchOne = await getScopeLeafTitle(page, 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { applyScopes, openScopesSelector, selectScope } from '../utils/scope-helpers';
|
||||
import { applyScopes, openScopesSelector, selectScope, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopesWithRedirect } from '../utils/scopes';
|
||||
|
||||
test.use({
|
||||
@@ -16,8 +16,13 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should redirect to custom URL when scope has redirectUrl', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
// Set up routes BEFORE navigation to ensure all requests are mocked
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -40,8 +45,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should prioritize redirectUrl over scope navigation fallback', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -68,8 +77,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -112,8 +125,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -151,9 +168,13 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should not redirect to redirectPath when on active scope navigation', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Set up scope navigation to dashboard-1', async () => {
|
||||
// First, apply a scope that creates scope navigation to dashboard-1 (without redirectPath)
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-setup', scopes[2]);
|
||||
await applyScopes(page, [scopes[2]]);
|
||||
|
||||
@@ -51,6 +51,8 @@ test.describe('Dashboard keybindings with new layouts', { tag: ['@dashboards'] }
|
||||
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible();
|
||||
|
||||
// Press Escape to close tooltip on the close button
|
||||
await page.keyboard.press('Escape');
|
||||
// Press Escape to close inspector
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ test.describe(
|
||||
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible();
|
||||
|
||||
// Press Escape to close tooltip on the close button
|
||||
await page.keyboard.press('Escape');
|
||||
// Press Escape to close inspector
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
|
||||
|
||||
// click cell inspect, check that cell inspection pops open in the side as we'd expect.
|
||||
await loremIpsumCell.getByLabel('Inspect value').click();
|
||||
const loremIpsumText = await loremIpsumCell.textContent();
|
||||
expect(loremIpsumText).toBeDefined();
|
||||
await loremIpsumCell.getByLabel('Inspect value').click();
|
||||
await expect(page.getByRole('dialog').getByText(loremIpsumText!)).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,150 @@ import { Resource } from '../../public/app/features/apiserver/types';
|
||||
|
||||
import { testScopes } from './scopes';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CALLS_CONFIG_PATH);
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
|
||||
/**
|
||||
* Sets up all scope-related API routes before navigation.
|
||||
* This ensures that ALL scope API requests (including those made during initial page load)
|
||||
* are intercepted by the mocks, preventing RTK Query from caching real API responses.
|
||||
*
|
||||
* Call this BEFORE navigating to a page (e.g., before gotoDashboardPage).
|
||||
*/
|
||||
export async function setupScopeRoutes(page: Page, scopes: TestScope[]): Promise<void> {
|
||||
// Route for scope node children (tree structure)
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_node_children*`, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const parentParam = url.searchParams.get('parent');
|
||||
const queryParam = url.searchParams.get('query');
|
||||
|
||||
// Find the appropriate scopes based on parent
|
||||
let scopesToReturn = scopes;
|
||||
if (parentParam) {
|
||||
// Find nested scopes based on parent name
|
||||
const findChildren = (items: TestScope[]): TestScope[] => {
|
||||
for (const item of items) {
|
||||
if (item.name === parentParam && item.children) {
|
||||
return item.children;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findChildren(item.children);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
scopesToReturn = findChildren(scopes);
|
||||
if (scopesToReturn.length === 0) {
|
||||
scopesToReturn = scopes; // Fallback to root scopes
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search query if provided
|
||||
if (queryParam) {
|
||||
const query = queryParam.toLowerCase();
|
||||
const filterByQuery = (items: TestScope[]): TestScope[] => {
|
||||
const results: TestScope[] = [];
|
||||
for (const item of items) {
|
||||
// Exact match on name or title containing the query
|
||||
if (item.name.toLowerCase() === query || item.title.toLowerCase() === query) {
|
||||
results.push(item);
|
||||
} else if (item.name.toLowerCase().includes(query) || item.title.toLowerCase().includes(query)) {
|
||||
results.push(item);
|
||||
}
|
||||
// Also search in children
|
||||
if (item.children) {
|
||||
results.push(...filterByQuery(item.children));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
scopesToReturn = filterByQuery(scopesToReturn);
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
kind: 'FindScopeNodeChildrenResults',
|
||||
metadata: {},
|
||||
items: scopesToReturn.map((scope) => ({
|
||||
kind: 'ScopeNode',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: scope.name,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: scope.title,
|
||||
disableMultiSelect: scope.disableMultiSelect ?? false,
|
||||
nodeType: scope.children ? 'container' : 'leaf',
|
||||
...(parentParam && { parentName: parentParam }),
|
||||
...((scope.addLinks || scope.children) && {
|
||||
linkType: 'scope',
|
||||
linkId: `scope-${scope.name}`,
|
||||
}),
|
||||
...(scope.redirectPath && { redirectPath: scope.redirectPath }),
|
||||
},
|
||||
})),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Route for individual scope fetching
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/*`, async (route) => {
|
||||
const url = route.request().url();
|
||||
const scopeName = url.split('/scopes/')[1]?.split('?')[0];
|
||||
|
||||
// Find the scope in the test data
|
||||
const findScope = (items: TestScope[]): TestScope | undefined => {
|
||||
for (const item of items) {
|
||||
if (`scope-${item.name}` === scopeName) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findScope(item.children);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const scope = findScope(scopes);
|
||||
|
||||
if (scope) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
kind: 'Scope',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: `scope-${scope.name}`,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: '',
|
||||
filters: scope.filters,
|
||||
category: scope.category,
|
||||
type: scope.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({ status: 404 });
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Dashboard bindings and navigations routes are set up dynamically in applyScopes()
|
||||
// with scope-specific URL patterns to avoid cache issues. They are not set up here.
|
||||
}
|
||||
|
||||
export type TestScope = {
|
||||
name: string;
|
||||
@@ -24,6 +167,9 @@ export type TestScope = {
|
||||
|
||||
type ScopeDashboardBinding = Resource<ScopeDashboardBindingSpec, ScopeDashboardBindingStatus, 'ScopeDashboardBinding'>;
|
||||
|
||||
/**
|
||||
* Sets up a route for scope node children requests and waits for the response.
|
||||
*/
|
||||
export async function scopeNodeChildrenRequest(
|
||||
page: Page,
|
||||
scopes: TestScope[],
|
||||
@@ -68,10 +214,13 @@ export async function scopeNodeChildrenRequest(
|
||||
return page.waitForResponse((response) => response.url().includes(`/find/scope_node_children`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the scopes selector dropdown and waits for the tree to load.
|
||||
*/
|
||||
export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-selector-input').click();
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -82,10 +231,13 @@ export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a scope tree node and waits for children to load.
|
||||
*/
|
||||
export async function expandScopesSelection(page: Page, parentScope: string, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId(`scopes-tree-${parentScope}-expand`).click();
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -96,6 +248,9 @@ export async function expandScopesSelection(page: Page, parentScope: string, sco
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a route for individual scope requests and waits for the response.
|
||||
*/
|
||||
export async function scopeSelectRequest(page: Page, selectedScope: TestScope): Promise<Response> {
|
||||
await page.route(
|
||||
`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/scope-${selectedScope.name}`,
|
||||
@@ -125,6 +280,9 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope):
|
||||
return page.waitForResponse((response) => response.url().includes(`/scopes/scope-${selectedScope.name}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a scope in the tree.
|
||||
*/
|
||||
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
|
||||
const click = async () => {
|
||||
const element = page.locator(
|
||||
@@ -134,7 +292,7 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await element.click({ force: true });
|
||||
};
|
||||
|
||||
if (!selectedScope) {
|
||||
if (!selectedScope || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -145,14 +303,22 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected scopes and waits for the selector to close and page to settle.
|
||||
* Sets up routes dynamically with scope-specific URL patterns to avoid cache issues.
|
||||
*/
|
||||
export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => {
|
||||
await page.getByTestId('scopes-selector-apply').scrollIntoViewIfNeeded();
|
||||
await page.getByTestId('scopes-selector-apply').click({ force: true });
|
||||
};
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls (dashboard bindings, etc.) to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,7 +332,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
|
||||
const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', ''];
|
||||
|
||||
// Mock scope_dashboard_bindings endpoint
|
||||
// Mock scope_dashboard_bindings endpoint with scope-specific URL pattern
|
||||
await page.route(dashboardBindingsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -220,7 +386,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
});
|
||||
});
|
||||
|
||||
// Mock scope_navigations endpoint
|
||||
// Mock scope_navigations endpoint with scope-specific URL pattern
|
||||
await page.route(scopeNavigationsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -266,21 +432,23 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
(response) =>
|
||||
response.url().includes(`/find/scope_dashboard_bindings`) || response.url().includes(`/find/scope_navigations`)
|
||||
);
|
||||
const scopeRequestPromises: Array<Promise<Response>> = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
scopeRequestPromises.push(scopeSelectRequest(page, scope));
|
||||
}
|
||||
|
||||
await click();
|
||||
await responsePromise;
|
||||
await Promise.all(scopeRequestPromises);
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
export async function searchScopes(page: Page, value: string, resultScopes: TestScope[]) {
|
||||
/**
|
||||
* Searches for scopes in the tree and waits for results.
|
||||
* Sets up a route dynamically with filtered results to return only matching scopes.
|
||||
*/
|
||||
export async function searchScopes(page: Page, value: string, resultScopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-tree-search').fill(value);
|
||||
|
||||
if (!resultScopes) {
|
||||
if (!resultScopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = false
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -1021,11 +1021,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/core/actions/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/core/components/AccessControl/PermissionList.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -1161,11 +1156,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/core/config.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/core/navigation/types.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -2610,11 +2600,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/hooks/useStateSync/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/spec/helper/setup.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -4030,11 +4015,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/parca/webpack.config.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -4103,7 +4083,7 @@
|
||||
"count": 1
|
||||
},
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/tempo/resultTransformer.ts": {
|
||||
|
||||
@@ -585,6 +585,8 @@ module.exports = [
|
||||
// FIXME: Remove once all enterprise issues are fixed -
|
||||
// we don't have a suppressions file/approach for enterprise code yet
|
||||
...enterpriseIgnores,
|
||||
// Ignore decoupled plugin webpack configs
|
||||
'public/app/**/webpack.config.ts',
|
||||
],
|
||||
rules: {
|
||||
'no-barrel-files/no-barrel-files': 'error',
|
||||
|
||||
@@ -32,14 +32,14 @@ require (
|
||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
@@ -82,14 +82,14 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
|
||||
github.com/golang/snappy v1.0.0 // @grafana/alerting-backend
|
||||
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/google/go-github/v70 v70.0.0 // indirect; @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent
|
||||
github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group
|
||||
github.com/google/wire v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -113,6 +113,7 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // @grafana/grafana-backend-group
|
||||
@@ -260,12 +261,13 @@ require (
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0 // @grafana/plugins-platform-backend
|
||||
|
||||
// This needs to be here for other projects that import grafana/grafana
|
||||
// For local development grafana/grafana will always use the local files
|
||||
// Check go.work file for details
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
)
|
||||
|
||||
// Replace the workspace versions
|
||||
@@ -294,6 +296,8 @@ replace (
|
||||
github.com/grafana/grafana/pkg/aggregator => ./pkg/aggregator
|
||||
github.com/grafana/grafana/pkg/apimachinery => ./pkg/apimachinery
|
||||
github.com/grafana/grafana/pkg/apiserver => ./pkg/apiserver
|
||||
github.com/grafana/grafana/pkg/plugins => ./pkg/plugins
|
||||
github.com/grafana/grafana/pkg/semconv => ./pkg/semconv
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -652,11 +656,12 @@ require (
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
|
||||
github.com/Machiel/slugify v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
@@ -676,6 +681,8 @@ require (
|
||||
github.com/google/gnostic v0.7.1 // indirect
|
||||
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // indirect
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
@@ -697,7 +704,7 @@ require (
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
// Use our fork of the upstream Alertmanager.
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
|
||||
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
|
||||
|
||||
@@ -680,6 +680,7 @@ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7Og
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
@@ -737,6 +738,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
|
||||
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
@@ -759,6 +762,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
|
||||
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@@ -1026,6 +1031,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -1620,8 +1627,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
@@ -1664,8 +1671,6 @@ github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDP
|
||||
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
|
||||
@@ -1676,8 +1681,8 @@ github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ
|
||||
github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
|
||||
@@ -1753,6 +1758,8 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
@@ -1877,6 +1884,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
|
||||
@@ -38,6 +38,6 @@ use (
|
||||
./pkg/semconv
|
||||
)
|
||||
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
+5
-1
@@ -280,7 +280,6 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
|
||||
@@ -906,6 +905,8 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3 h1:KVncUdAc5YwY/OQmw6HgzJmbRKn6IwrhvtcBAd1yDHo=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3/go.mod h1:Oy4MthJqfErlieO14ryZXdukDrUACy8Lg56P3zP7S1k=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
@@ -996,6 +997,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
@@ -1911,6 +1914,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
|
||||
|
||||
+2
-4
@@ -62,8 +62,7 @@
|
||||
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
|
||||
"storybook": "yarn workspace @grafana/ui storybook --ci",
|
||||
"storybook:build": "yarn workspace @grafana/ui storybook:build",
|
||||
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
|
||||
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes-generate": "yarn workspace @grafana/data themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
|
||||
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
|
||||
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
|
||||
@@ -254,7 +253,6 @@
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
"webpack": "5.101.0",
|
||||
"webpack-assets-manifest": "^5.1.0",
|
||||
"webpack-cli": "6.0.1",
|
||||
@@ -265,7 +263,7 @@
|
||||
"webpackbar": "^7.0.0",
|
||||
"yaml": "^2.0.0",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^4.0.0"
|
||||
"zod": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bsull/augurs": "^0.10.0",
|
||||
|
||||
@@ -34,6 +34,8 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
|
||||
getBackendSrv().fetch({
|
||||
...requestOptions,
|
||||
url: baseURL + requestOptions.url,
|
||||
// Default to GET so backend_srv correctly skips success alerts for queries
|
||||
method: requestOptions.method ?? 'GET',
|
||||
showErrorAlert: requestOptions.showErrorAlert ?? false,
|
||||
data: requestOptions.body,
|
||||
headers,
|
||||
|
||||
+6
@@ -246,6 +246,8 @@ const injectedRtkApi = api
|
||||
facetLimit: queryArg.facetLimit,
|
||||
tags: queryArg.tags,
|
||||
libraryPanel: queryArg.libraryPanel,
|
||||
panelType: queryArg.panelType,
|
||||
dataSourceType: queryArg.dataSourceType,
|
||||
permission: queryArg.permission,
|
||||
sort: queryArg.sort,
|
||||
limit: queryArg.limit,
|
||||
@@ -674,6 +676,10 @@ export type SearchDashboardsAndFoldersApiArg = {
|
||||
tags?: string[];
|
||||
/** find dashboards that reference a given libraryPanel */
|
||||
libraryPanel?: string;
|
||||
/** find dashboards using panels of a given plugin type */
|
||||
panelType?: string;
|
||||
/** find dashboards using datasources of a given plugin type */
|
||||
dataSourceType?: string;
|
||||
/** permission needed for the resource (view, edit, admin) */
|
||||
permission?: 'view' | 'edit' | 'admin';
|
||||
/** sortable field */
|
||||
|
||||
@@ -5340,6 +5340,7 @@ export type OrgUserDto = {
|
||||
};
|
||||
authLabels?: string[];
|
||||
avatarUrl?: string;
|
||||
created?: string;
|
||||
email?: string;
|
||||
isDisabled?: boolean;
|
||||
isExternallySynced?: boolean;
|
||||
@@ -6118,6 +6119,7 @@ export type ChangeUserPasswordCommand = {
|
||||
export type UserSearchHitDto = {
|
||||
authLabels?: string[];
|
||||
avatarUrl?: string;
|
||||
created?: string;
|
||||
email?: string;
|
||||
id?: number;
|
||||
isAdmin?: boolean;
|
||||
|
||||
+1
-1
@@ -1452,7 +1452,7 @@ export type ConnectionSecure = {
|
||||
/** PrivateKey is the reference to the private key used for GitHub App authentication. This value is stored securely and cannot be read back */
|
||||
privateKey?: InlineSecureValue;
|
||||
/** Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back */
|
||||
webhook?: InlineSecureValue;
|
||||
token?: InlineSecureValue;
|
||||
};
|
||||
export type BitbucketConnectionConfig = {
|
||||
/** App client ID */
|
||||
|
||||
@@ -47,11 +47,12 @@
|
||||
"LICENSE_APACHE2"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"build": "yarn themes-schema && tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
@@ -81,10 +82,12 @@
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.8.1",
|
||||
"uplot": "1.6.32",
|
||||
"xss": "^1.0.14"
|
||||
"xss": "^1.0.14",
|
||||
"zod": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/scenes": "6.38.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@types/history": "4.7.11",
|
||||
@@ -101,6 +104,7 @@
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json from '@rollup/plugin-json';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
||||
@@ -8,13 +9,13 @@ const pkg = rq('./package.json');
|
||||
export default [
|
||||
{
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FieldColorModeId } from '../types/fieldColor';
|
||||
import { FieldConfigPropertyItem, FieldConfigSource } from '../types/fieldOverrides';
|
||||
import { InterpolateFunction } from '../types/panel';
|
||||
import { ThresholdsMode } from '../types/thresholds';
|
||||
import { MappingType } from '../types/valueMapping';
|
||||
import { Registry } from '../utils/Registry';
|
||||
import { locationUtil } from '../utils/location';
|
||||
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
|
||||
@@ -999,6 +1000,45 @@ describe('setDynamicConfigValue', () => {
|
||||
expect(config.custom.property3).toEqual({});
|
||||
expect(config.displayName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('works correctly with multiple value mappings in the same override', () => {
|
||||
const config: FieldConfig = {
|
||||
mappings: [{ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } }],
|
||||
};
|
||||
|
||||
setDynamicConfigValue(
|
||||
config,
|
||||
{
|
||||
id: 'mappings',
|
||||
value: [{ type: MappingType.ValueToText, options: { first: { text: 'first' } } }],
|
||||
},
|
||||
{
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
data: [],
|
||||
field: { type: FieldType.number } as Field,
|
||||
dataFrameIndex: 0,
|
||||
}
|
||||
);
|
||||
|
||||
setDynamicConfigValue(
|
||||
config,
|
||||
{
|
||||
id: 'mappings',
|
||||
value: [{ type: MappingType.ValueToText, options: { second: { text: 'second' } } }],
|
||||
},
|
||||
{
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
data: [],
|
||||
field: { type: FieldType.number } as Field,
|
||||
dataFrameIndex: 0,
|
||||
}
|
||||
);
|
||||
|
||||
expect(config.mappings).toHaveLength(3);
|
||||
expect(config.mappings![0]).toEqual({ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } });
|
||||
expect(config.mappings![1]).toEqual({ type: MappingType.ValueToText, options: { first: { text: 'first' } } });
|
||||
expect(config.mappings![2]).toEqual({ type: MappingType.ValueToText, options: { second: { text: 'second' } } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinksSupplier', () => {
|
||||
|
||||
@@ -341,7 +341,7 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV
|
||||
return;
|
||||
}
|
||||
|
||||
const val = item.process(value.value, context, item.settings);
|
||||
let val = item.process(value.value, context, item.settings);
|
||||
|
||||
const remove = val === undefined || val === null;
|
||||
|
||||
@@ -352,6 +352,15 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV
|
||||
unset(config, item.path);
|
||||
}
|
||||
} else {
|
||||
// Merge arrays (e.g. mappings) when multiple overrides target the same field
|
||||
if (Array.isArray(val)) {
|
||||
const existingValue = item.isCustom ? get(config.custom, item.path) : get(config, item.path);
|
||||
|
||||
if (Array.isArray(existingValue)) {
|
||||
val = [...existingValue, ...val];
|
||||
}
|
||||
}
|
||||
|
||||
if (item.isCustom) {
|
||||
if (!config.custom) {
|
||||
config.custom = {};
|
||||
|
||||
@@ -106,3 +106,4 @@ export { findNumericFieldMinMax } from '../field/fieldOverrides';
|
||||
export { type PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||
export { sanitize, sanitizeUrl } from '../text/sanitize';
|
||||
export { type NestedValueAccess, type NestedPanelOptions, isNestedPanelOptions } from '../utils/OptionsUIBuilders';
|
||||
export { NewThemeOptionsSchema } from '../themes/createTheme';
|
||||
|
||||
@@ -1,83 +1,103 @@
|
||||
import { merge } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { alpha, darken, emphasize, getContrastRatio, lighten } from './colorManipulator';
|
||||
import { palette } from './palette';
|
||||
import { DeepPartial, ThemeRichColor } from './types';
|
||||
import { DeepRequired, ThemeRichColor, ThemeRichColorInputSchema } from './types';
|
||||
|
||||
const ThemeColorsModeSchema = z.enum(['light', 'dark']);
|
||||
/** @internal */
|
||||
export type ThemeColorsMode = 'light' | 'dark';
|
||||
export type ThemeColorsMode = z.infer<typeof ThemeColorsModeSchema>;
|
||||
|
||||
const createThemeColorsBaseSchema = <TColor>(color: TColor) =>
|
||||
z
|
||||
.object({
|
||||
mode: ThemeColorsModeSchema,
|
||||
|
||||
primary: color,
|
||||
secondary: color,
|
||||
info: color,
|
||||
error: color,
|
||||
success: color,
|
||||
warning: color,
|
||||
|
||||
text: z.object({
|
||||
primary: z.string().optional(),
|
||||
secondary: z.string().optional(),
|
||||
disabled: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: z.string().optional(),
|
||||
}),
|
||||
|
||||
background: z.object({
|
||||
/** Dashboard and body background */
|
||||
canvas: z.string().optional(),
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: z.string().optional(),
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: z.string().optional(),
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: z.string().optional(),
|
||||
}),
|
||||
|
||||
border: z.object({
|
||||
weak: z.string().optional(),
|
||||
medium: z.string().optional(),
|
||||
strong: z.string().optional(),
|
||||
}),
|
||||
|
||||
gradients: z.object({
|
||||
brandVertical: z.string().optional(),
|
||||
brandHorizontal: z.string().optional(),
|
||||
}),
|
||||
|
||||
action: z.object({
|
||||
/** Used for selected menu item / select option */
|
||||
selected: z.string().optional(),
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: z.string().optional(),
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: z.string().optional(),
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: z.number().optional(),
|
||||
/** Used focused menu item / select option */
|
||||
focus: z.string().optional(),
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: z.string().optional(),
|
||||
/** Disabled text */
|
||||
disabledText: z.string().optional(),
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: z.number().optional(),
|
||||
}),
|
||||
|
||||
hoverFactor: z.number(),
|
||||
contrastThreshold: z.number(),
|
||||
tonalOffset: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
// Need to override the zod type to include the generic properly
|
||||
/** @internal */
|
||||
export interface ThemeColorsBase<TColor> {
|
||||
mode: ThemeColorsMode;
|
||||
|
||||
export type ThemeColorsBase<TColor> = DeepRequired<
|
||||
Omit<
|
||||
z.infer<ReturnType<typeof createThemeColorsBaseSchema>>,
|
||||
'primary' | 'secondary' | 'info' | 'error' | 'success' | 'warning'
|
||||
>
|
||||
> & {
|
||||
primary: TColor;
|
||||
secondary: TColor;
|
||||
info: TColor;
|
||||
error: TColor;
|
||||
success: TColor;
|
||||
warning: TColor;
|
||||
|
||||
text: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
disabled: string;
|
||||
link: string;
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: string;
|
||||
};
|
||||
|
||||
background: {
|
||||
/** Dashboard and body background */
|
||||
canvas: string;
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: string;
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: string;
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: string;
|
||||
};
|
||||
|
||||
border: {
|
||||
weak: string;
|
||||
medium: string;
|
||||
strong: string;
|
||||
};
|
||||
|
||||
gradients: {
|
||||
brandVertical: string;
|
||||
brandHorizontal: string;
|
||||
};
|
||||
|
||||
action: {
|
||||
/** Used for selected menu item / select option */
|
||||
selected: string;
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: string;
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: string;
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: number;
|
||||
/** Used focused menu item / select option */
|
||||
focus: string;
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: string;
|
||||
/** Disabled text */
|
||||
disabledText: string;
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: number;
|
||||
};
|
||||
|
||||
hoverFactor: number;
|
||||
contrastThreshold: number;
|
||||
tonalOffset: number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ThemeHoverStrengh {}
|
||||
|
||||
@@ -89,8 +109,10 @@ export interface ThemeColors extends ThemeColorsBase<ThemeRichColor> {
|
||||
emphasize(color: string, amount?: number): string;
|
||||
}
|
||||
|
||||
export const ThemeColorsInputSchema = createThemeColorsBaseSchema(ThemeRichColorInputSchema);
|
||||
|
||||
/** @internal */
|
||||
export type ThemeColorsInput = DeepPartial<ThemeColorsBase<ThemeRichColor>>;
|
||||
export type ThemeColorsInput = z.infer<typeof ThemeColorsInputSchema>;
|
||||
|
||||
class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
|
||||
mode: ThemeColorsMode = 'dark';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @beta */
|
||||
export interface ThemeShape {
|
||||
/**
|
||||
@@ -34,9 +36,12 @@ export interface Radii {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ThemeShapeInput {
|
||||
borderRadius?: number;
|
||||
}
|
||||
export const ThemeShapeInputSchema = z.object({
|
||||
borderRadius: z.int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeShapeInput = z.infer<typeof ThemeShapeInputSchema>;
|
||||
|
||||
export function createShape(options: ThemeShapeInput): ThemeShape {
|
||||
const baseBorderRadius = options.borderRadius ?? 6;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingOptions = {
|
||||
gridSize?: number;
|
||||
};
|
||||
export const ThemeSpacingOptionsSchema = z.object({
|
||||
gridSize: z.int().positive().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingOptions = z.infer<typeof ThemeSpacingOptionsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingArgument = number | string;
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createBreakpoints } from './breakpoints';
|
||||
import { createColors, ThemeColorsInput } from './createColors';
|
||||
import { createColors, ThemeColorsInputSchema } from './createColors';
|
||||
import { createComponents } from './createComponents';
|
||||
import { createShadows } from './createShadows';
|
||||
import { createShape, ThemeShapeInput } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptions } from './createSpacing';
|
||||
import { createShape, ThemeShapeInputSchema } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptionsSchema } from './createSpacing';
|
||||
import { createTransitions } from './createTransitions';
|
||||
import { createTypography, ThemeTypographyInput } from './createTypography';
|
||||
import { createTypography, ThemeTypographyInputSchema } from './createTypography';
|
||||
import { createV1Theme } from './createV1Theme';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInputSchema } from './createVisualizationColors';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
import { zIndex } from './zIndex';
|
||||
|
||||
/** @internal */
|
||||
export interface NewThemeOptions {
|
||||
name?: string;
|
||||
colors?: ThemeColorsInput;
|
||||
spacing?: ThemeSpacingOptions;
|
||||
shape?: ThemeShapeInput;
|
||||
typography?: ThemeTypographyInput;
|
||||
visualization?: ThemeVisualizationColorsInput;
|
||||
}
|
||||
export const NewThemeOptionsSchema = z.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
colors: ThemeColorsInputSchema.optional(),
|
||||
spacing: ThemeSpacingOptionsSchema.optional(),
|
||||
shape: ThemeShapeInputSchema.optional(),
|
||||
typography: ThemeTypographyInputSchema.optional(),
|
||||
visualization: ThemeVisualizationColorsInputSchema.optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
|
||||
export type NewThemeOptions = z.infer<typeof NewThemeOptionsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export function createTheme(
|
||||
options: Omit<NewThemeOptions, 'id' | 'name'> & {
|
||||
name?: NewThemeOptions['name'];
|
||||
} = {}
|
||||
): GrafanaTheme2 {
|
||||
const {
|
||||
name,
|
||||
colors: colorsInput = {},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
|
||||
@@ -40,18 +41,20 @@ export interface ThemeTypographyVariant {
|
||||
letterSpacing?: string;
|
||||
}
|
||||
|
||||
export interface ThemeTypographyInput {
|
||||
fontFamily?: string;
|
||||
fontFamilyMonospace?: string;
|
||||
fontSize?: number;
|
||||
fontWeightLight?: number;
|
||||
fontWeightRegular?: number;
|
||||
fontWeightMedium?: number;
|
||||
fontWeightBold?: number;
|
||||
// hat's the font-size on the html element.
|
||||
export const ThemeTypographyInputSchema = z.object({
|
||||
fontFamily: z.string().optional(),
|
||||
fontFamilyMonospace: z.string().optional(),
|
||||
fontSize: z.number().positive().optional(),
|
||||
fontWeightLight: z.number().positive().optional(),
|
||||
fontWeightRegular: z.number().positive().optional(),
|
||||
fontWeightMedium: z.number().positive().optional(),
|
||||
fontWeightBold: z.number().positive().optional(),
|
||||
// what's the font-size on the html element.
|
||||
// 16px is the default font-size used by browsers.
|
||||
htmlFontSize?: number;
|
||||
}
|
||||
htmlFontSize: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type ThemeTypographyInput = z.infer<typeof ThemeTypographyInputSchema>;
|
||||
|
||||
const defaultFontFamily = "'Inter', 'Helvetica', 'Arial', sans-serif";
|
||||
const defaultFontFamilyMonospace = "'Roboto Mono', monospace";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FALLBACK_COLOR } from '../types/fieldColor';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
@@ -26,29 +28,44 @@ export interface ThemeVizColor<T extends ThemeVizColorName> {
|
||||
|
||||
type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';
|
||||
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> =
|
||||
| `super-light-${T}`
|
||||
| `light-${T}`
|
||||
| T
|
||||
| `semi-dark-${T}`
|
||||
| `dark-${T}`;
|
||||
const createShadeSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.enum([`super-light-${color}`, `light-${color}`, color, `semi-dark-${color}`, `dark-${color}`]);
|
||||
|
||||
type ThemeVizHueGeneric<T> = T extends ThemeVizColorName
|
||||
? {
|
||||
name: T;
|
||||
shades: Array<ThemeVizColor<T>>;
|
||||
}
|
||||
: never;
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> = z.infer<ReturnType<typeof createShadeSchema<T>>>;
|
||||
|
||||
const createHueSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.object({
|
||||
name: z.literal(color),
|
||||
shades: z.array(
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: createShadeSchema(color),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
primary: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const ThemeVizHueSchema = z.union([
|
||||
createHueSchema('red'),
|
||||
createHueSchema('orange'),
|
||||
createHueSchema('yellow'),
|
||||
createHueSchema('green'),
|
||||
createHueSchema('blue'),
|
||||
createHueSchema('purple'),
|
||||
]);
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
|
||||
export type ThemeVizHue = z.infer<typeof ThemeVizHueSchema>;
|
||||
|
||||
export type ThemeVisualizationColorsInput = {
|
||||
hues?: ThemeVizHue[];
|
||||
palette?: string[];
|
||||
};
|
||||
export const ThemeVisualizationColorsInputSchema = z.object({
|
||||
hues: z.array(ThemeVizHueSchema).optional(),
|
||||
palette: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type ThemeVisualizationColorsInput = z.infer<typeof ThemeVisualizationColorsInputSchema>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme } from './createTheme';
|
||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
||||
import * as extraThemes from './themeDefinitions';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
|
||||
@@ -42,9 +42,6 @@ export function getBuiltInThemes(allowedExtras: string[]) {
|
||||
return sortedThemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is also a backend list at pkg/services/preference/themes.go
|
||||
*/
|
||||
const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
return [
|
||||
{ id: 'system', name: 'System preference', build: getSystemPreferenceTheme },
|
||||
@@ -53,13 +50,19 @@ const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
for (const [id, theme] of Object.entries(extraThemes)) {
|
||||
themeRegistry.register({
|
||||
id,
|
||||
name: theme.name ?? '',
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
for (const [name, json] of Object.entries(extraThemes)) {
|
||||
const result = NewThemeOptionsSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
|
||||
} else {
|
||||
const theme = result.data;
|
||||
themeRegistry.register({
|
||||
id: theme.id,
|
||||
name: theme.name,
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemPreferenceTheme() {
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark"]
|
||||
},
|
||||
"primary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"secondary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"warning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxContrast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"background": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canvas": {
|
||||
"type": "string"
|
||||
},
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"elevated": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"border": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"weak": {
|
||||
"type": "string"
|
||||
},
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"strong": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"gradients": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"brandVertical": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandHorizontal": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selected": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectedBorder": {
|
||||
"type": "string"
|
||||
},
|
||||
"hover": {
|
||||
"type": "string"
|
||||
},
|
||||
"hoverOpacity": {
|
||||
"type": "number"
|
||||
},
|
||||
"focus": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledBackground": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledText": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledOpacity": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"hoverFactor": {
|
||||
"type": "number"
|
||||
},
|
||||
"contrastThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"tonalOffset": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"spacing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gridSize": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"shape": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"borderRadius": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"typography": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fontFamily": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontFamilyMonospace": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightLight": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightRegular": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightMedium": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightBold": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"htmlFontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"visualization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hues": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "red"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-red", "light-red", "red", "semi-dark-red", "dark-red"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "orange"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-orange", "light-orange", "orange", "semi-dark-orange", "dark-orange"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "yellow"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-yellow", "light-yellow", "yellow", "semi-dark-yellow", "dark-yellow"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "green"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-green", "light-green", "green", "semi-dark-green", "dark-green"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "blue"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-blue", "light-blue", "blue", "semi-dark-blue", "dark-blue"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "purple"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-purple", "light-purple", "purple", "semi-dark-purple", "dark-purple"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"palette": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "id"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { NewThemeOptionsSchema } from '../createTheme';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../schema.generated.json'),
|
||||
JSON.stringify(
|
||||
NewThemeOptionsSchema.toJSONSchema({
|
||||
target: 'draft-07',
|
||||
}),
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Aubergine",
|
||||
"id": "aubergine",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#4F2A3D",
|
||||
"medium": "#6A3C4B",
|
||||
"strong": "#8C5A69"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E5D0D6",
|
||||
"secondary": "#D1A8C4",
|
||||
"disabled": "#B7A0A6",
|
||||
"link": "#A56BB6",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#8C5A69"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#6A3C4B",
|
||||
"text": "#D1A8C4",
|
||||
"border": "#8C5A69"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#2E1F2D",
|
||||
"primary": "#3C2136",
|
||||
"secondary": "#4A2D47",
|
||||
"elevated": "#4A2D47"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#6A3C4B",
|
||||
"selected": "#8C5A69",
|
||||
"selectedBorder": "#FFB300",
|
||||
"focus": "#A56BB6",
|
||||
"hoverOpacity": 0.1,
|
||||
"disabledText": "#B7A0A6",
|
||||
"disabledBackground": "#4A2D47",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)"
|
||||
},
|
||||
"contrastThreshold": 4,
|
||||
"hoverFactor": 0.07,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const aubergineTheme: NewThemeOptions = {
|
||||
name: 'Aubergine',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#4F2A3D',
|
||||
medium: '#6A3C4B',
|
||||
strong: '#8C5A69',
|
||||
},
|
||||
text: {
|
||||
primary: '#E5D0D6',
|
||||
secondary: '#D1A8C4',
|
||||
disabled: '#B7A0A6',
|
||||
link: '#A56BB6',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#8C5A69',
|
||||
},
|
||||
secondary: {
|
||||
main: '#6A3C4B',
|
||||
text: '#D1A8C4',
|
||||
border: '#8C5A69',
|
||||
},
|
||||
background: {
|
||||
canvas: '#2E1F2D',
|
||||
primary: '#3C2136',
|
||||
secondary: '#4A2D47',
|
||||
elevated: '#4A2D47',
|
||||
},
|
||||
action: {
|
||||
hover: '#6A3C4B',
|
||||
selected: '#8C5A69',
|
||||
selectedBorder: '#FFB300',
|
||||
focus: '#A56BB6',
|
||||
hoverOpacity: 0.1,
|
||||
disabledText: '#B7A0A6',
|
||||
disabledBackground: '#4A2D47',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
},
|
||||
contrastThreshold: 4,
|
||||
hoverFactor: 0.07,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default aubergineTheme;
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "Debug",
|
||||
"id": "debug",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"background": {
|
||||
"canvas": "#000033",
|
||||
"primary": "#000044",
|
||||
"secondary": "#000055",
|
||||
"elevated": "#000055"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#bbbb00",
|
||||
"secondary": "#888800",
|
||||
"disabled": "#444400",
|
||||
"link": "#dddd00",
|
||||
"maxContrast": "#ffff00"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#ff000044",
|
||||
"medium": "#ff000088",
|
||||
"strong": "#ff0000ff"
|
||||
},
|
||||
"primary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"secondary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"info": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"warning": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"success": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"error": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#9900dd",
|
||||
"focus": "#6600aa",
|
||||
"selected": "#440088"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 8
|
||||
},
|
||||
"spacing": {
|
||||
"gridSize": 10
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user