Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 731d776a99 | |||
| c308b3bac4 | |||
| c154654162 | |||
| 1fe9a38a2a | |||
| 59bf7896f4 | |||
| f6dfbe0e15 | |||
| b90b8f2a34 | |||
| b473524787 | |||
| cb05a4ae1b | |||
| 84a07be6e4 | |||
| a8aef11926 | |||
| f116539541 | |||
| a455f9700d | |||
| 399370bc2c | |||
| b08e7bc373 | |||
| 7eb467e561 | |||
| d732bb2751 | |||
| c29ed31c7a | |||
| ebaccc781b | |||
| 4b4ad544a8 | |||
| ca8cad68c8 | |||
| 51d562eb81 | |||
| 4130bd9cd3 | |||
| 759d49a1df | |||
| 84fbe6bc7b | |||
| 9606e9c51c | |||
| d49993ddab | |||
| f0a394e67b | |||
| c94bf34d0b | |||
| 5ba3139d4a | |||
| e1a2f178e7 | |||
| 7e3289f2c9 | |||
| 0d0b5b757b | |||
| ae2e5f0df7 | |||
| 21c1d9aedd | |||
| 8c7170727b | |||
| c49261cce2 | |||
| d5efce72f3 | |||
| 881c81f0b3 | |||
| cef4449f14 | |||
| a3dacabedf | |||
| 291e3ea9cf | |||
| 5538dfe73d | |||
| 513b81a531 | |||
| 136a0eb4d6 | |||
| bfda534825 | |||
| d226c35904 | |||
| a99946d921 | |||
| 2544a3441e | |||
| fee3f12e4e | |||
| 235ebbf5c5 | |||
| da374527f2 | |||
| 6e0093f048 | |||
| ccba9fd70b | |||
| a6497cf91e | |||
| 5694d6371e | |||
| 82b81c2b75 | |||
| c42d6a53a6 | |||
| 2e524483ac | |||
| 93ec32dd6a | |||
| 9091ac6f5c | |||
| eb25d9f4a8 | |||
| 1672bbada0 | |||
| 1d4067216d |
@@ -185,6 +185,7 @@
|
||||
/pkg/services/search/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/searchusers/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/secrets/ @grafana/grafana-operator-experience-squad
|
||||
/pkg/services/setting/ @grafana/grafana-backend-services-squad
|
||||
/pkg/services/shorturls/ @grafana/sharing-squad
|
||||
/pkg/services/sqlstore/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/ssosettings/ @grafana/identity-squad
|
||||
|
||||
@@ -33,16 +33,48 @@ runs:
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-enterprise.git ../grafana-enterprise;
|
||||
RETRIES="5"
|
||||
|
||||
for i in $(seq 1 $RETRIES); do
|
||||
echo "Attempt $i to clone..."
|
||||
|
||||
if git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-enterprise.git ../grafana-enterprise; then
|
||||
echo "Clone succeeded on attempt $i"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $i -eq $RETRIES ]; then
|
||||
echo "Clone failed after $RETRIES attempts, failing pipeline."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$i"
|
||||
done
|
||||
|
||||
cd ../grafana-enterprise
|
||||
|
||||
if git checkout ${GITHUB_HEAD_REF}; then
|
||||
echo "checked out ${GITHUB_HEAD_REF}"
|
||||
elif git checkout ${GITHUB_BASE_REF}; then
|
||||
echo "checked out ${GITHUB_BASE_REF}"
|
||||
else
|
||||
git checkout main
|
||||
fi
|
||||
for i in $(seq 1 $RETRIES); do
|
||||
echo "Attempt $i to checkout..."
|
||||
|
||||
if git checkout ${GITHUB_HEAD_REF}; then
|
||||
echo "checked out ${GITHUB_HEAD_REF}"
|
||||
elif git checkout ${GITHUB_BASE_REF}; then
|
||||
echo "checked out ${GITHUB_BASE_REF}"
|
||||
else
|
||||
git checkout main
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Checkout succeeded, breaking retry loop"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $i -eq $RETRIES ]; then
|
||||
echo "Checkout failed after $RETRIES attempts, failing pipeline."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$i"
|
||||
done
|
||||
|
||||
QUIET=1 ./build.sh
|
||||
|
||||
@@ -107,6 +107,7 @@ jobs:
|
||||
CURRENT_OSS_WIRE_CHECKSUM=$(sha256sum pkg/server/wire_gen.go | cut -d' ' -f1)
|
||||
if [ "$CURRENT_OSS_WIRE_CHECKSUM" != "${{ steps.pre_gen_oss.outputs.wire_checksum }}" ]; then
|
||||
OSS_WIRE_CHANGED=true
|
||||
OSS_DIFF=$(git diff pkg/server/wire_gen.go)
|
||||
echo "Uncomitted changes detected in pkg/server/wire_gen.go"
|
||||
fi
|
||||
|
||||
@@ -115,6 +116,7 @@ jobs:
|
||||
CURRENT_ENTERPRISE_WIRE_CHECKSUM=$(sha256sum pkg/server/enterprise_wire_gen.go | cut -d' ' -f1)
|
||||
if [ "$CURRENT_ENTERPRISE_WIRE_CHECKSUM" != "${{ steps.pre_gen_enterprise.outputs.wire_checksum }}" ]; then
|
||||
ENTERPRISE_WIRE_CHANGED=true
|
||||
ENTERPRISE_DIFF=$(git diff pkg/server/enterprise_wire_gen.go)
|
||||
echo "Uncomitted changes detected in pkg/server/enterprise_wire_gen.go"
|
||||
fi
|
||||
fi
|
||||
@@ -124,6 +126,14 @@ jobs:
|
||||
else
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "false" ]]; then
|
||||
echo "> !!! Please synchronize the grafana OSS and grafana enterprise code bases as defined in the enterprise readme, then run 'make gen-go' in the OSS folder and commit the changes to both repositories."
|
||||
if [[ "$OSS_WIRE_CHANGED" == "true" ]]; then
|
||||
echo "OSS changes:"
|
||||
echo "$OSS_DIFF"
|
||||
fi
|
||||
if [[ "$ENTERPRISE_WIRE_CHANGED" == "true" ]]; then
|
||||
echo "Enterprise changes:"
|
||||
echo "$ENTERPRISE_DIFF"
|
||||
fi
|
||||
else
|
||||
echo "> !!! Please run 'make gen-go' and commit the changes."
|
||||
fi
|
||||
|
||||
@@ -71,6 +71,7 @@ public/css/*.min.css
|
||||
.vs/
|
||||
.cursor/
|
||||
.devcontainer/
|
||||
.claude/
|
||||
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
@@ -165,6 +166,7 @@ pkg/services/quota/quotaimpl/storage/storage.json
|
||||
/packages/grafana-ui/.yarn/.cache
|
||||
/packages/grafana-ui/.storybook/static
|
||||
/packages/grafana-ui/unstable
|
||||
/packages/grafana-flamegraph/.storybook/static
|
||||
/packages/**/dist
|
||||
/packages/**/compiled
|
||||
/packages/**/.rpt2_cache
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ require (
|
||||
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
|
||||
github.com/grafana/grafana-app-sdk v0.48.2
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
|
||||
+2
-2
@@ -624,8 +624,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
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=
|
||||
|
||||
@@ -7,7 +7,7 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
|
||||
github.com/grafana/grafana-app-sdk v0.48.2
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
||||
@@ -89,8 +89,8 @@ github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJH
|
||||
github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
|
||||
@@ -66,6 +66,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
|
||||
}
|
||||
|
||||
func formatErrorPath(path []string) string {
|
||||
if len(path) <= 4 {
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
// omitting the "lineage.schemas[0].schema.spec" prefix here.
|
||||
return strings.Join(path[4:], ".")
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis
|
||||
}
|
||||
|
||||
func formatErrorPath(path []string) string {
|
||||
if len(path) <= 4 {
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
// omitting the "lineage.schemas[0].schema.spec" prefix here.
|
||||
return strings.Join(path[4:], ".")
|
||||
}
|
||||
|
||||
+5
-5
@@ -14,16 +14,12 @@ replace github.com/grafana/grafana/apps/provisioning => ../provisioning
|
||||
|
||||
replace github.com/grafana/grafana/apps/advisor => ../advisor
|
||||
|
||||
replace github.com/grafana/grafana/apps/annotation => ../annotation
|
||||
|
||||
replace github.com/grafana/grafana/apps/alerting/alertenrichment => ../alerting/alertenrichment
|
||||
|
||||
replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/notifications
|
||||
|
||||
replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules
|
||||
|
||||
replace github.com/grafana/grafana/apps/collections => ../collections
|
||||
|
||||
replace github.com/grafana/grafana/apps/correlations => ../correlations
|
||||
|
||||
replace github.com/grafana/grafana/apps/investigations => ../investigations
|
||||
@@ -46,6 +42,10 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver
|
||||
|
||||
replace github.com/grafana/grafana/pkg/aggregator => ../../pkg/aggregator
|
||||
|
||||
replace github.com/grafana/grafana/apps/annotation => ../annotation
|
||||
|
||||
replace github.com/grafana/grafana/apps/collections => ../collections
|
||||
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
|
||||
require (
|
||||
@@ -225,7 +225,7 @@ require (
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/plugins v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||
|
||||
+2
-8
@@ -847,8 +847,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg
|
||||
github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
@@ -1410,8 +1410,6 @@ github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQcc
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0 h1:ScvS2Ga9snVkpOahln/BCLySr3/iBAHJf25u66DweZ0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0/go.mod h1:Dxkqms3OJadP5zirIBPLi9FV8Qpys3T3w40XPEcVsu0=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc=
|
||||
@@ -1534,10 +1532,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
||||
@@ -18,4 +18,18 @@ teamv0alpha1: teamKind & {
|
||||
schema: {
|
||||
spec: v0alpha1.TeamSpec
|
||||
}
|
||||
routes: {
|
||||
"/groups": {
|
||||
"GET": {
|
||||
response: {
|
||||
#ExternalGroupMapping: {
|
||||
name: string
|
||||
externalGroup: string
|
||||
}
|
||||
items: [...#ExternalGroupMapping]
|
||||
}
|
||||
responseMetadata: objectMeta: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,7 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error {
|
||||
&TeamBindingList{},
|
||||
&ExternalGroupMapping{},
|
||||
&ExternalGroupMappingList{},
|
||||
&GetGroups{},
|
||||
// For now these are registered in pkg/apis/iam/v0alpha1/register.go
|
||||
// &UserTeamList{},
|
||||
// &ServiceAccountTokenList{},
|
||||
|
||||
+24
@@ -2,6 +2,9 @@ package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
@@ -78,3 +81,24 @@ func (c *TeamClient) Patch(ctx context.Context, identifier resource.Identifier,
|
||||
func (c *TeamClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
|
||||
type GetGroupsRequest struct {
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func (c *TeamClient) GetGroups(ctx context.Context, identifier resource.Identifier, request GetGroupsRequest) (*GetGroups, error) {
|
||||
resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{
|
||||
Path: "/groups",
|
||||
Verb: "GET",
|
||||
Headers: request.Headers,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cast := GetGroups{}
|
||||
err = json.Unmarshal(resp, &cast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal response bytes into GetGroups: %w", err)
|
||||
}
|
||||
return &cast, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping struct {
|
||||
Name string `json:"name"`
|
||||
ExternalGroup string `json:"externalGroup"`
|
||||
}
|
||||
|
||||
// NewVersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping creates a new VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping object.
|
||||
func NewVersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping() *VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping {
|
||||
return &VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetGroupsBody struct {
|
||||
Items []VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping `json:"items"`
|
||||
}
|
||||
|
||||
// NewGetGroupsBody creates a new GetGroupsBody object.
|
||||
func NewGetGroupsBody() *GetGroupsBody {
|
||||
return &GetGroupsBody{
|
||||
Items: []VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetGroups struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
GetGroupsBody `json:",inline"`
|
||||
}
|
||||
|
||||
func NewGetGroups() *GetGroups {
|
||||
return &GetGroups{}
|
||||
}
|
||||
|
||||
func (t *GetGroupsBody) DeepCopyInto(dst *GetGroupsBody) {
|
||||
_ = resource.CopyObjectInto(dst, t)
|
||||
}
|
||||
|
||||
func (o *GetGroups) DeepCopyObject() runtime.Object {
|
||||
dst := NewGetGroups()
|
||||
o.DeepCopyInto(dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (o *GetGroups) DeepCopyInto(dst *GetGroups) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.GetGroupsBody.DeepCopyInto(&dst.GetGroupsBody)
|
||||
}
|
||||
|
||||
var _ runtime.Object = NewGetGroups()
|
||||
+165
-65
@@ -12,71 +12,74 @@ import (
|
||||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRole": schema_pkg_apis_iam_v0alpha1_CoreRole(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleList": schema_pkg_apis_iam_v0alpha1_CoreRoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec": schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": schema_pkg_apis_iam_v0alpha1_CoreRoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": schema_pkg_apis_iam_v0alpha1_CoreRolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecRoleRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleList": schema_pkg_apis_iam_v0alpha1_GlobalRoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolespecPermission": schema_pkg_apis_iam_v0alpha1_GlobalRolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermission": schema_pkg_apis_iam_v0alpha1_ResourcePermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionList": schema_pkg_apis_iam_v0alpha1_ResourcePermissionList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec": schema_pkg_apis_iam_v0alpha1_ResourcePermissionSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus": schema_pkg_apis_iam_v0alpha1_ResourcePermissionStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecPermission": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecResource": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecResource(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ResourcePermissionstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Role": schema_pkg_apis_iam_v0alpha1_Role(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBinding": schema_pkg_apis_iam_v0alpha1_RoleBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingList": schema_pkg_apis_iam_v0alpha1_RoleBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec": schema_pkg_apis_iam_v0alpha1_RoleBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus": schema_pkg_apis_iam_v0alpha1_RoleBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_RoleBindingspecRoleRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_RoleBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_RoleBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleList": schema_pkg_apis_iam_v0alpha1_RoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec": schema_pkg_apis_iam_v0alpha1_RoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": schema_pkg_apis_iam_v0alpha1_RoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": schema_pkg_apis_iam_v0alpha1_RolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRole": schema_pkg_apis_iam_v0alpha1_CoreRole(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleList": schema_pkg_apis_iam_v0alpha1_CoreRoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec": schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": schema_pkg_apis_iam_v0alpha1_CoreRoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": schema_pkg_apis_iam_v0alpha1_CoreRolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroups": schema_pkg_apis_iam_v0alpha1_GetGroups(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecRoleRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleList": schema_pkg_apis_iam_v0alpha1_GlobalRoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolespecPermission": schema_pkg_apis_iam_v0alpha1_GlobalRolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermission": schema_pkg_apis_iam_v0alpha1_ResourcePermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionList": schema_pkg_apis_iam_v0alpha1_ResourcePermissionList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec": schema_pkg_apis_iam_v0alpha1_ResourcePermissionSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus": schema_pkg_apis_iam_v0alpha1_ResourcePermissionStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecPermission": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecResource": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecResource(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ResourcePermissionstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Role": schema_pkg_apis_iam_v0alpha1_Role(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBinding": schema_pkg_apis_iam_v0alpha1_RoleBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingList": schema_pkg_apis_iam_v0alpha1_RoleBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec": schema_pkg_apis_iam_v0alpha1_RoleBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus": schema_pkg_apis_iam_v0alpha1_RoleBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_RoleBindingspecRoleRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_RoleBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_RoleBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleList": schema_pkg_apis_iam_v0alpha1_RoleList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec": schema_pkg_apis_iam_v0alpha1_RoleSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": schema_pkg_apis_iam_v0alpha1_RoleStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": schema_pkg_apis_iam_v0alpha1_RolespecPermission(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +494,76 @@ func schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref common.Referen
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GetGroups(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -2856,3 +2929,30 @@ func schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref common.ReferenceCa
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"externalGroup": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "externalGroup"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+55
-1
@@ -81,6 +81,58 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Teams",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Routes: map[string]spec3.PathProps{
|
||||
"/groups": {
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
|
||||
OperationId: "getGroups",
|
||||
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
Default: &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Default OK response",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
},
|
||||
},
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"items",
|
||||
"apiVersion",
|
||||
"kind",
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -142,7 +194,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{}
|
||||
var customRouteToGoResponseType = map[string]any{
|
||||
"v0alpha1|Team|groups|GET": v0alpha1.GetGroups{},
|
||||
}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
|
||||
|
||||
+15
-1
@@ -22,6 +22,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||
@@ -32,15 +34,20 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/smithy-go v1.23.1 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
@@ -74,7 +81,7 @@ require (
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
@@ -121,11 +128,15 @@ require (
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/nikunjy/rules v1.5.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/open-feature/go-sdk v1.16.0 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
@@ -142,6 +153,7 @@ require (
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect
|
||||
github.com/tjhop/slog-gokit v0.1.5 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -159,6 +171,8 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
|
||||
+34
-2
@@ -1,7 +1,11 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -9,6 +13,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4=
|
||||
github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
@@ -31,12 +37,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
@@ -52,12 +64,16 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
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/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
|
||||
@@ -169,8 +185,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
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/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
@@ -310,6 +326,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw=
|
||||
github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
@@ -324,6 +342,12 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
|
||||
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 h1:megzzlQGjsRVWDX8oJnLaa5eEcsAHekiL4Uvl3jSAcY=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6/go.mod h1:K1gDKvt76CGFLSUMHUydd5ba2V5Cv69gQZsdbnXhAm8=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
@@ -397,6 +421,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM=
|
||||
github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8=
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc=
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM=
|
||||
github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc=
|
||||
github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
@@ -446,8 +474,12 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
package plugins
|
||||
|
||||
pluginMetaV0Alpha1: {
|
||||
kind: "PluginMeta"
|
||||
kind: "PluginMeta"
|
||||
plural: "pluginsmeta"
|
||||
scope: "Namespaced"
|
||||
scope: "Namespaced"
|
||||
schema: {
|
||||
spec: {
|
||||
pluginJSON: #JSONData,
|
||||
pluginJson: #JSONData
|
||||
module?: {
|
||||
path: string
|
||||
hash?: string
|
||||
loadingStrategy?: "fetch" | "script"
|
||||
}
|
||||
baseURL?: string
|
||||
signature?: {
|
||||
status: "internal" | "valid" | "invalid" | "modified" | "unsigned"
|
||||
type?: "grafana" | "commercial" | "community" | "private" | "private-glob"
|
||||
org?: string
|
||||
}
|
||||
angular?: {
|
||||
detected: bool
|
||||
}
|
||||
translations?: [string]: string
|
||||
// +listType=atomic
|
||||
children?: [...string]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
-2
@@ -207,13 +207,20 @@ func NewPluginMetaExtensions() *PluginMetaExtensions {
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaSpec struct {
|
||||
PluginJSON PluginMetaJSONData `json:"pluginJSON"`
|
||||
PluginJson PluginMetaJSONData `json:"pluginJson"`
|
||||
Module *PluginMetaV0alpha1SpecModule `json:"module,omitempty"`
|
||||
BaseURL *string `json:"baseURL,omitempty"`
|
||||
Signature *PluginMetaV0alpha1SpecSignature `json:"signature,omitempty"`
|
||||
Angular *PluginMetaV0alpha1SpecAngular `json:"angular,omitempty"`
|
||||
Translations map[string]string `json:"translations,omitempty"`
|
||||
// +listType=atomic
|
||||
Children []string `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaSpec creates a new PluginMetaSpec object.
|
||||
func NewPluginMetaSpec() *PluginMetaSpec {
|
||||
return &PluginMetaSpec{
|
||||
PluginJSON: *NewPluginMetaJSONData(),
|
||||
PluginJson: *NewPluginMetaJSONData(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +418,40 @@ func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1Extensi
|
||||
return &PluginMetaV0alpha1ExtensionsExtensionPoints{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecModule struct {
|
||||
Path string `json:"path"`
|
||||
Hash *string `json:"hash,omitempty"`
|
||||
LoadingStrategy *PluginMetaV0alpha1SpecModuleLoadingStrategy `json:"loadingStrategy,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecModule creates a new PluginMetaV0alpha1SpecModule object.
|
||||
func NewPluginMetaV0alpha1SpecModule() *PluginMetaV0alpha1SpecModule {
|
||||
return &PluginMetaV0alpha1SpecModule{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignature struct {
|
||||
Status PluginMetaV0alpha1SpecSignatureStatus `json:"status"`
|
||||
Type *PluginMetaV0alpha1SpecSignatureType `json:"type,omitempty"`
|
||||
Org *string `json:"org,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecSignature creates a new PluginMetaV0alpha1SpecSignature object.
|
||||
func NewPluginMetaV0alpha1SpecSignature() *PluginMetaV0alpha1SpecSignature {
|
||||
return &PluginMetaV0alpha1SpecSignature{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecAngular struct {
|
||||
Detected bool `json:"detected"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecAngular creates a new PluginMetaV0alpha1SpecAngular object.
|
||||
func NewPluginMetaV0alpha1SpecAngular() *PluginMetaV0alpha1SpecAngular {
|
||||
return &PluginMetaV0alpha1SpecAngular{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONDataType string
|
||||
|
||||
@@ -471,3 +512,33 @@ const (
|
||||
PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource"
|
||||
PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecModuleLoadingStrategy string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecModuleLoadingStrategyFetch PluginMetaV0alpha1SpecModuleLoadingStrategy = "fetch"
|
||||
PluginMetaV0alpha1SpecModuleLoadingStrategyScript PluginMetaV0alpha1SpecModuleLoadingStrategy = "script"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignatureStatus string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecSignatureStatusInternal PluginMetaV0alpha1SpecSignatureStatus = "internal"
|
||||
PluginMetaV0alpha1SpecSignatureStatusValid PluginMetaV0alpha1SpecSignatureStatus = "valid"
|
||||
PluginMetaV0alpha1SpecSignatureStatusInvalid PluginMetaV0alpha1SpecSignatureStatus = "invalid"
|
||||
PluginMetaV0alpha1SpecSignatureStatusModified PluginMetaV0alpha1SpecSignatureStatus = "modified"
|
||||
PluginMetaV0alpha1SpecSignatureStatusUnsigned PluginMetaV0alpha1SpecSignatureStatus = "unsigned"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignatureType string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecSignatureTypeGrafana PluginMetaV0alpha1SpecSignatureType = "grafana"
|
||||
PluginMetaV0alpha1SpecSignatureTypeCommercial PluginMetaV0alpha1SpecSignatureType = "commercial"
|
||||
PluginMetaV0alpha1SpecSignatureTypeCommunity PluginMetaV0alpha1SpecSignatureType = "community"
|
||||
PluginMetaV0alpha1SpecSignatureTypePrivate PluginMetaV0alpha1SpecSignatureType = "private"
|
||||
PluginMetaV0alpha1SpecSignatureTypePrivateGlob PluginMetaV0alpha1SpecSignatureType = "private-glob"
|
||||
)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -87,8 +87,47 @@ func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: gcomMeta.JSON,
|
||||
}
|
||||
|
||||
if gcomMeta.VersionSignatureType != "" {
|
||||
signature := &pluginsv0alpha1.PluginMetaV0alpha1SpecSignature{
|
||||
Status: pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusValid,
|
||||
}
|
||||
|
||||
switch gcomMeta.VersionSignatureType {
|
||||
case "grafana":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
signature.Type = &sigType
|
||||
case "commercial":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommercial
|
||||
signature.Type = &sigType
|
||||
case "community":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommunity
|
||||
signature.Type = &sigType
|
||||
case "private":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivate
|
||||
signature.Type = &sigType
|
||||
case "private-glob":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivateGlob
|
||||
signature.Type = &sigType
|
||||
}
|
||||
|
||||
if gcomMeta.VersionSignedByOrg != "" {
|
||||
signature.Org = &gcomMeta.VersionSignedByOrg
|
||||
}
|
||||
|
||||
spec.Signature = signature
|
||||
}
|
||||
|
||||
// Set angular info
|
||||
spec.Angular = &pluginsv0alpha1.PluginMetaV0alpha1SpecAngular{
|
||||
Detected: gcomMeta.AngularDetected,
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Meta: gcomMeta.JSON,
|
||||
Meta: spec,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
@@ -96,25 +135,25 @@ func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
// grafanaComPluginVersionMeta represents the response from grafana.com API
|
||||
// GET /api/plugins/{pluginId}/versions/{version}
|
||||
type grafanaComPluginVersionMeta struct {
|
||||
PluginID string `json:"pluginSlug"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
Commit string `json:"commit"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"`
|
||||
Readme string `json:"readme"`
|
||||
Downloads int `json:"downloads"`
|
||||
Verified bool `json:"verified"`
|
||||
Status string `json:"status"`
|
||||
StatusContext string `json:"statusContext"`
|
||||
DownloadSlug string `json:"downloadSlug"`
|
||||
SignatureType string `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
Packages struct {
|
||||
PluginID string `json:"pluginSlug"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
Commit string `json:"commit"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"`
|
||||
Readme string `json:"readme"`
|
||||
Downloads int `json:"downloads"`
|
||||
Verified bool `json:"verified"`
|
||||
Status string `json:"status"`
|
||||
StatusContext string `json:"statusContext"`
|
||||
DownloadSlug string `json:"downloadSlug"`
|
||||
VersionSignatureType string `json:"versionSignatureType"`
|
||||
VersionSignedByOrg string `json:"versionSignedByOrg"`
|
||||
VersionSignedByOrgName string `json:"versionSignedByOrgName"`
|
||||
Packages struct {
|
||||
Any struct {
|
||||
Md5 string `json:"md5"`
|
||||
Sha256 string `json:"sha256"`
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestCloudProvider_GetMeta(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, expectedMeta, result.Meta.PluginJson)
|
||||
assert.Equal(t, defaultCloudTTL, result.TTL)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData.
|
||||
// nolint:gocyclo
|
||||
func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData {
|
||||
meta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: jsonData.ID,
|
||||
Name: jsonData.Name,
|
||||
}
|
||||
|
||||
// Map plugin type
|
||||
switch jsonData.Type {
|
||||
case plugins.TypeApp:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp
|
||||
case plugins.TypeDataSource:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource
|
||||
case plugins.TypePanel:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel
|
||||
case plugins.TypeRenderer:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer
|
||||
}
|
||||
|
||||
// Map Info
|
||||
meta.Info = pluginsv0alpha1.PluginMetaInfo{
|
||||
Keywords: jsonData.Info.Keywords,
|
||||
Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{
|
||||
Small: jsonData.Info.Logos.Small,
|
||||
Large: jsonData.Info.Logos.Large,
|
||||
},
|
||||
Updated: jsonData.Info.Updated,
|
||||
Version: jsonData.Info.Version,
|
||||
}
|
||||
|
||||
if jsonData.Info.Description != "" {
|
||||
meta.Info.Description = &jsonData.Info.Description
|
||||
}
|
||||
|
||||
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
|
||||
author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{}
|
||||
if jsonData.Info.Author.Name != "" {
|
||||
author.Name = &jsonData.Info.Author.Name
|
||||
}
|
||||
if jsonData.Info.Author.URL != "" {
|
||||
author.Url = &jsonData.Info.Author.URL
|
||||
}
|
||||
meta.Info.Author = author
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Links) > 0 {
|
||||
meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
|
||||
for _, link := range jsonData.Info.Links {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{}
|
||||
if link.Name != "" {
|
||||
v0Link.Name = &link.Name
|
||||
}
|
||||
if link.URL != "" {
|
||||
v0Link.Url = &link.URL
|
||||
}
|
||||
meta.Info.Links = append(meta.Info.Links, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Screenshots) > 0 {
|
||||
meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
|
||||
for _, screenshot := range jsonData.Info.Screenshots {
|
||||
v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{}
|
||||
if screenshot.Name != "" {
|
||||
v0Screenshot.Name = &screenshot.Name
|
||||
}
|
||||
if screenshot.Path != "" {
|
||||
v0Screenshot.Path = &screenshot.Path
|
||||
}
|
||||
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Dependencies
|
||||
meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{
|
||||
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
|
||||
}
|
||||
|
||||
if jsonData.Dependencies.GrafanaVersion != "" {
|
||||
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Plugins) > 0 {
|
||||
meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
|
||||
for _, dep := range jsonData.Dependencies.Plugins {
|
||||
var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType
|
||||
switch dep.Type {
|
||||
case "app":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp
|
||||
case "datasource":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource
|
||||
case "panel":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel
|
||||
}
|
||||
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{
|
||||
Id: dep.ID,
|
||||
Type: depType,
|
||||
Name: dep.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
|
||||
meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{
|
||||
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Map optional boolean fields
|
||||
if jsonData.Alerting {
|
||||
meta.Alerting = &jsonData.Alerting
|
||||
}
|
||||
if jsonData.Annotations {
|
||||
meta.Annotations = &jsonData.Annotations
|
||||
}
|
||||
if jsonData.AutoEnabled {
|
||||
meta.AutoEnabled = &jsonData.AutoEnabled
|
||||
}
|
||||
if jsonData.Backend {
|
||||
meta.Backend = &jsonData.Backend
|
||||
}
|
||||
if jsonData.BuiltIn {
|
||||
meta.BuiltIn = &jsonData.BuiltIn
|
||||
}
|
||||
if jsonData.HideFromList {
|
||||
meta.HideFromList = &jsonData.HideFromList
|
||||
}
|
||||
if jsonData.Logs {
|
||||
meta.Logs = &jsonData.Logs
|
||||
}
|
||||
if jsonData.Metrics {
|
||||
meta.Metrics = &jsonData.Metrics
|
||||
}
|
||||
if jsonData.MultiValueFilterOperators {
|
||||
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
|
||||
}
|
||||
if jsonData.Preload {
|
||||
meta.Preload = &jsonData.Preload
|
||||
}
|
||||
if jsonData.SkipDataQuery {
|
||||
meta.SkipDataQuery = &jsonData.SkipDataQuery
|
||||
}
|
||||
if jsonData.Streaming {
|
||||
meta.Streaming = &jsonData.Streaming
|
||||
}
|
||||
if jsonData.Tracing {
|
||||
meta.Tracing = &jsonData.Tracing
|
||||
}
|
||||
|
||||
// Map category
|
||||
if jsonData.Category != "" {
|
||||
var category pluginsv0alpha1.PluginMetaJSONDataCategory
|
||||
switch jsonData.Category {
|
||||
case "tsdb":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb
|
||||
case "logging":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging
|
||||
case "cloud":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud
|
||||
case "tracing":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing
|
||||
case "profiling":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling
|
||||
case "sql":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategorySql
|
||||
case "enterprise":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise
|
||||
case "iot":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot
|
||||
case "other":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
default:
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
}
|
||||
meta.Category = &category
|
||||
}
|
||||
|
||||
// Map state
|
||||
if jsonData.State != "" {
|
||||
var state pluginsv0alpha1.PluginMetaJSONDataState
|
||||
switch jsonData.State {
|
||||
case plugins.ReleaseStateAlpha:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha
|
||||
case plugins.ReleaseStateBeta:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateBeta
|
||||
default:
|
||||
}
|
||||
if state != "" {
|
||||
meta.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
// Map executable
|
||||
if jsonData.Executable != "" {
|
||||
meta.Executable = &jsonData.Executable
|
||||
}
|
||||
|
||||
// Map QueryOptions
|
||||
if len(jsonData.QueryOptions) > 0 {
|
||||
queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{}
|
||||
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
|
||||
queryOptions.MaxDataPoints = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
|
||||
queryOptions.MinInterval = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
|
||||
queryOptions.CacheTimeout = &val
|
||||
}
|
||||
meta.QueryOptions = queryOptions
|
||||
}
|
||||
|
||||
// Map Includes
|
||||
if len(jsonData.Includes) > 0 {
|
||||
meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes))
|
||||
for _, include := range jsonData.Includes {
|
||||
v0Include := pluginsv0alpha1.PluginMetaInclude{}
|
||||
if include.UID != "" {
|
||||
v0Include.Uid = &include.UID
|
||||
}
|
||||
if include.Type != "" {
|
||||
var includeType pluginsv0alpha1.PluginMetaIncludeType
|
||||
switch include.Type {
|
||||
case "dashboard":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard
|
||||
case "page":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePage
|
||||
case "panel":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel
|
||||
case "datasource":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource
|
||||
}
|
||||
v0Include.Type = &includeType
|
||||
}
|
||||
if include.Name != "" {
|
||||
v0Include.Name = &include.Name
|
||||
}
|
||||
if include.Component != "" {
|
||||
v0Include.Component = &include.Component
|
||||
}
|
||||
if include.Role != "" {
|
||||
var role pluginsv0alpha1.PluginMetaIncludeRole
|
||||
switch include.Role {
|
||||
case "Admin":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin
|
||||
case "Editor":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleEditor
|
||||
case "Viewer":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleViewer
|
||||
}
|
||||
v0Include.Role = &role
|
||||
}
|
||||
if include.Action != "" {
|
||||
v0Include.Action = &include.Action
|
||||
}
|
||||
if include.Path != "" {
|
||||
v0Include.Path = &include.Path
|
||||
}
|
||||
if include.AddToNav {
|
||||
v0Include.AddToNav = &include.AddToNav
|
||||
}
|
||||
if include.DefaultNav {
|
||||
v0Include.DefaultNav = &include.DefaultNav
|
||||
}
|
||||
if include.Icon != "" {
|
||||
v0Include.Icon = &include.Icon
|
||||
}
|
||||
meta.Includes = append(meta.Includes, v0Include)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Routes
|
||||
if len(jsonData.Routes) > 0 {
|
||||
meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes))
|
||||
for _, route := range jsonData.Routes {
|
||||
v0Route := pluginsv0alpha1.PluginMetaRoute{}
|
||||
if route.Path != "" {
|
||||
v0Route.Path = &route.Path
|
||||
}
|
||||
if route.Method != "" {
|
||||
v0Route.Method = &route.Method
|
||||
}
|
||||
if route.URL != "" {
|
||||
v0Route.Url = &route.URL
|
||||
}
|
||||
if route.ReqRole != "" {
|
||||
reqRole := string(route.ReqRole)
|
||||
v0Route.ReqRole = &reqRole
|
||||
}
|
||||
if route.ReqAction != "" {
|
||||
v0Route.ReqAction = &route.ReqAction
|
||||
}
|
||||
if len(route.Headers) > 0 {
|
||||
headers := make([]string, 0, len(route.Headers))
|
||||
for _, header := range route.Headers {
|
||||
headers = append(headers, header.Name+": "+header.Content)
|
||||
}
|
||||
v0Route.Headers = headers
|
||||
}
|
||||
if len(route.URLParams) > 0 {
|
||||
v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
|
||||
for _, param := range route.URLParams {
|
||||
v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{}
|
||||
if param.Name != "" {
|
||||
v0Param.Name = ¶m.Name
|
||||
}
|
||||
if param.Content != "" {
|
||||
v0Param.Content = ¶m.Content
|
||||
}
|
||||
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
|
||||
}
|
||||
}
|
||||
if route.TokenAuth != nil {
|
||||
v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{}
|
||||
if route.TokenAuth.Url != "" {
|
||||
v0Route.TokenAuth.Url = &route.TokenAuth.Url
|
||||
}
|
||||
if len(route.TokenAuth.Scopes) > 0 {
|
||||
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
|
||||
}
|
||||
if len(route.TokenAuth.Params) > 0 {
|
||||
v0Route.TokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.TokenAuth.Params {
|
||||
v0Route.TokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.JwtTokenAuth != nil {
|
||||
v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
if route.JwtTokenAuth.Url != "" {
|
||||
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
|
||||
}
|
||||
if len(route.JwtTokenAuth.Scopes) > 0 {
|
||||
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
|
||||
}
|
||||
if len(route.JwtTokenAuth.Params) > 0 {
|
||||
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.JwtTokenAuth.Params {
|
||||
v0Route.JwtTokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(route.Body) > 0 {
|
||||
var bodyMap map[string]interface{}
|
||||
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
|
||||
v0Route.Body = bodyMap
|
||||
}
|
||||
}
|
||||
meta.Routes = append(meta.Routes, v0Route)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Extensions
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
|
||||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions := &pluginsv0alpha1.PluginMetaExtensions{}
|
||||
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 {
|
||||
extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
|
||||
for _, link := range jsonData.Extensions.AddedLinks {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: link.Targets,
|
||||
Title: link.Title,
|
||||
}
|
||||
if link.Description != "" {
|
||||
v0Link.Description = &link.Description
|
||||
}
|
||||
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.AddedComponents) > 0 {
|
||||
extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
|
||||
for _, comp := range jsonData.Extensions.AddedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: comp.Targets,
|
||||
Title: comp.Title,
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExposedComponents) > 0 {
|
||||
extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
|
||||
for _, comp := range jsonData.Extensions.ExposedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{
|
||||
Id: comp.Id,
|
||||
}
|
||||
if comp.Title != "" {
|
||||
v0Comp.Title = &comp.Title
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
|
||||
for _, point := range jsonData.Extensions.ExtensionPoints {
|
||||
v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{
|
||||
Id: point.Id,
|
||||
}
|
||||
if point.Title != "" {
|
||||
v0Point.Title = &point.Title
|
||||
}
|
||||
if point.Description != "" {
|
||||
v0Point.Description = &point.Description
|
||||
}
|
||||
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Extensions = extensions
|
||||
}
|
||||
|
||||
// Map Roles
|
||||
if len(jsonData.Roles) > 0 {
|
||||
meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles))
|
||||
for _, role := range jsonData.Roles {
|
||||
v0Role := pluginsv0alpha1.PluginMetaRole{
|
||||
Grants: role.Grants,
|
||||
}
|
||||
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{}
|
||||
if role.Role.Name != "" {
|
||||
v0RoleRole.Name = &role.Role.Name
|
||||
}
|
||||
if role.Role.Description != "" {
|
||||
v0RoleRole.Description = &role.Role.Description
|
||||
}
|
||||
if len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
|
||||
for _, perm := range role.Role.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
|
||||
}
|
||||
}
|
||||
v0Role.Role = v0RoleRole
|
||||
}
|
||||
meta.Roles = append(meta.Roles, v0Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Map IAM
|
||||
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
|
||||
iam := &pluginsv0alpha1.PluginMetaIAM{
|
||||
Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
|
||||
}
|
||||
for _, perm := range jsonData.IAM.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
iam.Permissions = append(iam.Permissions, v0Perm)
|
||||
}
|
||||
meta.Iam = iam
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
// pluginStorePluginToPluginMetaSpec converts a pluginstore.Plugin to a pluginsv0alpha1.PluginMetaSpec.
|
||||
// This is similar to pluginToPluginMetaSpec but works with the plugin store DTO.
|
||||
// loadingStrategy and moduleHash are optional calculated values that can be provided.
|
||||
func pluginStorePluginToPluginMetaSpec(plugin pluginstore.Plugin, loadingStrategy plugins.LoadingStrategy, moduleHash string) pluginsv0alpha1.PluginMetaSpec {
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: jsonDataToPluginMetaJSONData(plugin.JSONData),
|
||||
}
|
||||
|
||||
if plugin.Module != "" {
|
||||
module := &pluginsv0alpha1.PluginMetaV0alpha1SpecModule{
|
||||
Path: plugin.Module,
|
||||
}
|
||||
if moduleHash != "" {
|
||||
module.Hash = &moduleHash
|
||||
}
|
||||
if loadingStrategy != "" {
|
||||
var ls pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategy
|
||||
switch loadingStrategy {
|
||||
case plugins.LoadingStrategyFetch:
|
||||
ls = pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategyFetch
|
||||
case plugins.LoadingStrategyScript:
|
||||
ls = pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategyScript
|
||||
}
|
||||
module.LoadingStrategy = &ls
|
||||
}
|
||||
spec.Module = module
|
||||
}
|
||||
|
||||
if plugin.BaseURL != "" {
|
||||
spec.BaseURL = &plugin.BaseURL
|
||||
}
|
||||
|
||||
if plugin.Signature != "" {
|
||||
signature := &pluginsv0alpha1.PluginMetaV0alpha1SpecSignature{
|
||||
Status: convertSignatureStatus(plugin.Signature),
|
||||
}
|
||||
|
||||
if plugin.SignatureType != "" {
|
||||
sigType := convertSignatureType(plugin.SignatureType)
|
||||
signature.Type = &sigType
|
||||
}
|
||||
|
||||
if plugin.SignatureOrg != "" {
|
||||
signature.Org = &plugin.SignatureOrg
|
||||
}
|
||||
|
||||
spec.Signature = signature
|
||||
}
|
||||
|
||||
if len(plugin.Children) > 0 {
|
||||
spec.Children = plugin.Children
|
||||
}
|
||||
|
||||
spec.Angular = &pluginsv0alpha1.PluginMetaV0alpha1SpecAngular{
|
||||
Detected: plugin.Angular.Detected,
|
||||
}
|
||||
|
||||
if len(plugin.Translations) > 0 {
|
||||
spec.Translations = plugin.Translations
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// convertSignatureStatus converts plugins.SignatureStatus to pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatus.
|
||||
func convertSignatureStatus(status plugins.SignatureStatus) pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatus {
|
||||
switch status {
|
||||
case plugins.SignatureStatusInternal:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusInternal
|
||||
case plugins.SignatureStatusValid:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusValid
|
||||
case plugins.SignatureStatusInvalid:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusInvalid
|
||||
case plugins.SignatureStatusModified:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusModified
|
||||
case plugins.SignatureStatusUnsigned:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusUnsigned
|
||||
default:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusUnsigned
|
||||
}
|
||||
}
|
||||
|
||||
// convertSignatureType converts plugins.SignatureType to pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureType.
|
||||
func convertSignatureType(sigType plugins.SignatureType) pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureType {
|
||||
switch sigType {
|
||||
case plugins.SignatureTypeGrafana:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
case plugins.SignatureTypeCommercial:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommercial
|
||||
case plugins.SignatureTypeCommunity:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommunity
|
||||
case plugins.SignatureTypePrivate:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivate
|
||||
case plugins.SignatureTypePrivateGlob:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivateGlob
|
||||
default:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -23,7 +22,7 @@ const (
|
||||
// CoreProvider retrieves plugin metadata for core plugins.
|
||||
type CoreProvider struct {
|
||||
mu sync.RWMutex
|
||||
loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData
|
||||
loadedPlugins map[string]pluginsv0alpha1.PluginMetaSpec
|
||||
initialized bool
|
||||
ttl time.Duration
|
||||
}
|
||||
@@ -36,7 +35,7 @@ func NewCoreProvider() *CoreProvider {
|
||||
// NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL.
|
||||
func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider {
|
||||
return &CoreProvider{
|
||||
loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData),
|
||||
loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaSpec),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
@@ -76,9 +75,9 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result
|
||||
p.initialized = true
|
||||
}
|
||||
|
||||
if meta, found := p.loadedPlugins[pluginID]; found {
|
||||
if spec, found := p.loadedPlugins[pluginID]; found {
|
||||
return &Result{
|
||||
Meta: meta,
|
||||
Meta: spec,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
@@ -119,485 +118,11 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, bundle := range ps {
|
||||
meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData)
|
||||
p.loadedPlugins[bundle.Primary.JSONData.ID] = meta
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: jsonDataToPluginMetaJSONData(bundle.Primary.JSONData),
|
||||
}
|
||||
p.loadedPlugins[bundle.Primary.JSONData.ID] = spec
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData.
|
||||
// nolint:gocyclo
|
||||
func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData {
|
||||
meta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: jsonData.ID,
|
||||
Name: jsonData.Name,
|
||||
}
|
||||
|
||||
// Map plugin type
|
||||
switch jsonData.Type {
|
||||
case plugins.TypeApp:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp
|
||||
case plugins.TypeDataSource:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource
|
||||
case plugins.TypePanel:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel
|
||||
case plugins.TypeRenderer:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer
|
||||
}
|
||||
|
||||
// Map Info
|
||||
meta.Info = pluginsv0alpha1.PluginMetaInfo{
|
||||
Keywords: jsonData.Info.Keywords,
|
||||
Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{
|
||||
Small: jsonData.Info.Logos.Small,
|
||||
Large: jsonData.Info.Logos.Large,
|
||||
},
|
||||
Updated: jsonData.Info.Updated,
|
||||
Version: jsonData.Info.Version,
|
||||
}
|
||||
|
||||
if jsonData.Info.Description != "" {
|
||||
meta.Info.Description = &jsonData.Info.Description
|
||||
}
|
||||
|
||||
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
|
||||
author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{}
|
||||
if jsonData.Info.Author.Name != "" {
|
||||
author.Name = &jsonData.Info.Author.Name
|
||||
}
|
||||
if jsonData.Info.Author.URL != "" {
|
||||
author.Url = &jsonData.Info.Author.URL
|
||||
}
|
||||
meta.Info.Author = author
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Links) > 0 {
|
||||
meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
|
||||
for _, link := range jsonData.Info.Links {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{}
|
||||
if link.Name != "" {
|
||||
v0Link.Name = &link.Name
|
||||
}
|
||||
if link.URL != "" {
|
||||
v0Link.Url = &link.URL
|
||||
}
|
||||
meta.Info.Links = append(meta.Info.Links, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Screenshots) > 0 {
|
||||
meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
|
||||
for _, screenshot := range jsonData.Info.Screenshots {
|
||||
v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{}
|
||||
if screenshot.Name != "" {
|
||||
v0Screenshot.Name = &screenshot.Name
|
||||
}
|
||||
if screenshot.Path != "" {
|
||||
v0Screenshot.Path = &screenshot.Path
|
||||
}
|
||||
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Dependencies
|
||||
meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{
|
||||
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
|
||||
}
|
||||
|
||||
if jsonData.Dependencies.GrafanaVersion != "" {
|
||||
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Plugins) > 0 {
|
||||
meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
|
||||
for _, dep := range jsonData.Dependencies.Plugins {
|
||||
var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType
|
||||
switch dep.Type {
|
||||
case "app":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp
|
||||
case "datasource":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource
|
||||
case "panel":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel
|
||||
}
|
||||
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{
|
||||
Id: dep.ID,
|
||||
Type: depType,
|
||||
Name: dep.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
|
||||
meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{
|
||||
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Map optional boolean fields
|
||||
if jsonData.Alerting {
|
||||
meta.Alerting = &jsonData.Alerting
|
||||
}
|
||||
if jsonData.Annotations {
|
||||
meta.Annotations = &jsonData.Annotations
|
||||
}
|
||||
if jsonData.AutoEnabled {
|
||||
meta.AutoEnabled = &jsonData.AutoEnabled
|
||||
}
|
||||
if jsonData.Backend {
|
||||
meta.Backend = &jsonData.Backend
|
||||
}
|
||||
if jsonData.BuiltIn {
|
||||
meta.BuiltIn = &jsonData.BuiltIn
|
||||
}
|
||||
if jsonData.HideFromList {
|
||||
meta.HideFromList = &jsonData.HideFromList
|
||||
}
|
||||
if jsonData.Logs {
|
||||
meta.Logs = &jsonData.Logs
|
||||
}
|
||||
if jsonData.Metrics {
|
||||
meta.Metrics = &jsonData.Metrics
|
||||
}
|
||||
if jsonData.MultiValueFilterOperators {
|
||||
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
|
||||
}
|
||||
if jsonData.Preload {
|
||||
meta.Preload = &jsonData.Preload
|
||||
}
|
||||
if jsonData.SkipDataQuery {
|
||||
meta.SkipDataQuery = &jsonData.SkipDataQuery
|
||||
}
|
||||
if jsonData.Streaming {
|
||||
meta.Streaming = &jsonData.Streaming
|
||||
}
|
||||
if jsonData.Tracing {
|
||||
meta.Tracing = &jsonData.Tracing
|
||||
}
|
||||
|
||||
// Map category
|
||||
if jsonData.Category != "" {
|
||||
var category pluginsv0alpha1.PluginMetaJSONDataCategory
|
||||
switch jsonData.Category {
|
||||
case "tsdb":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb
|
||||
case "logging":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging
|
||||
case "cloud":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud
|
||||
case "tracing":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing
|
||||
case "profiling":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling
|
||||
case "sql":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategorySql
|
||||
case "enterprise":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise
|
||||
case "iot":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot
|
||||
case "other":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
default:
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
}
|
||||
meta.Category = &category
|
||||
}
|
||||
|
||||
// Map state
|
||||
if jsonData.State != "" {
|
||||
var state pluginsv0alpha1.PluginMetaJSONDataState
|
||||
switch jsonData.State {
|
||||
case plugins.ReleaseStateAlpha:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha
|
||||
case plugins.ReleaseStateBeta:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateBeta
|
||||
default:
|
||||
}
|
||||
if state != "" {
|
||||
meta.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
// Map executable
|
||||
if jsonData.Executable != "" {
|
||||
meta.Executable = &jsonData.Executable
|
||||
}
|
||||
|
||||
// Map QueryOptions
|
||||
if len(jsonData.QueryOptions) > 0 {
|
||||
queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{}
|
||||
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
|
||||
queryOptions.MaxDataPoints = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
|
||||
queryOptions.MinInterval = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
|
||||
queryOptions.CacheTimeout = &val
|
||||
}
|
||||
meta.QueryOptions = queryOptions
|
||||
}
|
||||
|
||||
// Map Includes
|
||||
if len(jsonData.Includes) > 0 {
|
||||
meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes))
|
||||
for _, include := range jsonData.Includes {
|
||||
v0Include := pluginsv0alpha1.PluginMetaInclude{}
|
||||
if include.UID != "" {
|
||||
v0Include.Uid = &include.UID
|
||||
}
|
||||
if include.Type != "" {
|
||||
var includeType pluginsv0alpha1.PluginMetaIncludeType
|
||||
switch include.Type {
|
||||
case "dashboard":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard
|
||||
case "page":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePage
|
||||
case "panel":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel
|
||||
case "datasource":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource
|
||||
}
|
||||
v0Include.Type = &includeType
|
||||
}
|
||||
if include.Name != "" {
|
||||
v0Include.Name = &include.Name
|
||||
}
|
||||
if include.Component != "" {
|
||||
v0Include.Component = &include.Component
|
||||
}
|
||||
if include.Role != "" {
|
||||
var role pluginsv0alpha1.PluginMetaIncludeRole
|
||||
switch include.Role {
|
||||
case "Admin":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin
|
||||
case "Editor":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleEditor
|
||||
case "Viewer":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleViewer
|
||||
}
|
||||
v0Include.Role = &role
|
||||
}
|
||||
if include.Action != "" {
|
||||
v0Include.Action = &include.Action
|
||||
}
|
||||
if include.Path != "" {
|
||||
v0Include.Path = &include.Path
|
||||
}
|
||||
if include.AddToNav {
|
||||
v0Include.AddToNav = &include.AddToNav
|
||||
}
|
||||
if include.DefaultNav {
|
||||
v0Include.DefaultNav = &include.DefaultNav
|
||||
}
|
||||
if include.Icon != "" {
|
||||
v0Include.Icon = &include.Icon
|
||||
}
|
||||
meta.Includes = append(meta.Includes, v0Include)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Routes
|
||||
if len(jsonData.Routes) > 0 {
|
||||
meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes))
|
||||
for _, route := range jsonData.Routes {
|
||||
v0Route := pluginsv0alpha1.PluginMetaRoute{}
|
||||
if route.Path != "" {
|
||||
v0Route.Path = &route.Path
|
||||
}
|
||||
if route.Method != "" {
|
||||
v0Route.Method = &route.Method
|
||||
}
|
||||
if route.URL != "" {
|
||||
v0Route.Url = &route.URL
|
||||
}
|
||||
if route.ReqRole != "" {
|
||||
reqRole := string(route.ReqRole)
|
||||
v0Route.ReqRole = &reqRole
|
||||
}
|
||||
if route.ReqAction != "" {
|
||||
v0Route.ReqAction = &route.ReqAction
|
||||
}
|
||||
if len(route.Headers) > 0 {
|
||||
headers := make([]string, 0, len(route.Headers))
|
||||
for _, header := range route.Headers {
|
||||
headers = append(headers, header.Name+": "+header.Content)
|
||||
}
|
||||
v0Route.Headers = headers
|
||||
}
|
||||
if len(route.URLParams) > 0 {
|
||||
v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
|
||||
for _, param := range route.URLParams {
|
||||
v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{}
|
||||
if param.Name != "" {
|
||||
v0Param.Name = ¶m.Name
|
||||
}
|
||||
if param.Content != "" {
|
||||
v0Param.Content = ¶m.Content
|
||||
}
|
||||
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
|
||||
}
|
||||
}
|
||||
if route.TokenAuth != nil {
|
||||
v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{}
|
||||
if route.TokenAuth.Url != "" {
|
||||
v0Route.TokenAuth.Url = &route.TokenAuth.Url
|
||||
}
|
||||
if len(route.TokenAuth.Scopes) > 0 {
|
||||
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
|
||||
}
|
||||
if len(route.TokenAuth.Params) > 0 {
|
||||
v0Route.TokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.TokenAuth.Params {
|
||||
v0Route.TokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.JwtTokenAuth != nil {
|
||||
v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
if route.JwtTokenAuth.Url != "" {
|
||||
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
|
||||
}
|
||||
if len(route.JwtTokenAuth.Scopes) > 0 {
|
||||
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
|
||||
}
|
||||
if len(route.JwtTokenAuth.Params) > 0 {
|
||||
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.JwtTokenAuth.Params {
|
||||
v0Route.JwtTokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(route.Body) > 0 {
|
||||
var bodyMap map[string]interface{}
|
||||
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
|
||||
v0Route.Body = bodyMap
|
||||
}
|
||||
}
|
||||
meta.Routes = append(meta.Routes, v0Route)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Extensions
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
|
||||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions := &pluginsv0alpha1.PluginMetaExtensions{}
|
||||
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 {
|
||||
extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
|
||||
for _, link := range jsonData.Extensions.AddedLinks {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: link.Targets,
|
||||
Title: link.Title,
|
||||
}
|
||||
if link.Description != "" {
|
||||
v0Link.Description = &link.Description
|
||||
}
|
||||
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.AddedComponents) > 0 {
|
||||
extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
|
||||
for _, comp := range jsonData.Extensions.AddedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: comp.Targets,
|
||||
Title: comp.Title,
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExposedComponents) > 0 {
|
||||
extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
|
||||
for _, comp := range jsonData.Extensions.ExposedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{
|
||||
Id: comp.Id,
|
||||
}
|
||||
if comp.Title != "" {
|
||||
v0Comp.Title = &comp.Title
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
|
||||
for _, point := range jsonData.Extensions.ExtensionPoints {
|
||||
v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{
|
||||
Id: point.Id,
|
||||
}
|
||||
if point.Title != "" {
|
||||
v0Point.Title = &point.Title
|
||||
}
|
||||
if point.Description != "" {
|
||||
v0Point.Description = &point.Description
|
||||
}
|
||||
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Extensions = extensions
|
||||
}
|
||||
|
||||
// Map Roles
|
||||
if len(jsonData.Roles) > 0 {
|
||||
meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles))
|
||||
for _, role := range jsonData.Roles {
|
||||
v0Role := pluginsv0alpha1.PluginMetaRole{
|
||||
Grants: role.Grants,
|
||||
}
|
||||
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{}
|
||||
if role.Role.Name != "" {
|
||||
v0RoleRole.Name = &role.Role.Name
|
||||
}
|
||||
if role.Role.Description != "" {
|
||||
v0RoleRole.Description = &role.Role.Description
|
||||
}
|
||||
if len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
|
||||
for _, perm := range role.Role.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
|
||||
}
|
||||
}
|
||||
v0Role.Role = v0RoleRole
|
||||
}
|
||||
meta.Roles = append(meta.Roles, v0Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Map IAM
|
||||
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
|
||||
iam := &pluginsv0alpha1.PluginMetaIAM{
|
||||
Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
|
||||
}
|
||||
for _, perm := range jsonData.IAM.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
iam.Permissions = append(iam.Permissions, v0Perm)
|
||||
}
|
||||
meta.Iam = iam
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
@@ -22,14 +22,16 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
t.Run("returns cached plugin when available", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -37,7 +39,7 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, expectedSpec, result.Meta)
|
||||
assert.Equal(t, defaultCoreTTL, result.TTL)
|
||||
})
|
||||
|
||||
@@ -58,14 +60,16 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
t.Run("ignores version parameter", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -81,14 +85,16 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
provider := NewCoreProviderWithTTL(customTTL)
|
||||
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -226,8 +232,8 @@ func TestCoreProvider_loadPlugins(t *testing.T) {
|
||||
if loaded {
|
||||
result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-datasource", result.Meta.Id)
|
||||
assert.Equal(t, "Test Datasource", result.Meta.Name)
|
||||
assert.Equal(t, "test-datasource", result.Meta.PluginJson.Id)
|
||||
assert.Equal(t, "Test Datasource", result.Meta.PluginJson.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLocalTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// PluginAssetsCalculator is an interface for calculating plugin asset information.
|
||||
// LocalProvider requires this to calculate loading strategy and module hash.
|
||||
type PluginAssetsCalculator interface {
|
||||
LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy
|
||||
ModuleHash(ctx context.Context, p pluginstore.Plugin) string
|
||||
}
|
||||
|
||||
// LocalProvider retrieves plugin metadata for locally installed plugins.
|
||||
// It uses the plugin store to access plugins that have already been loaded.
|
||||
type LocalProvider struct {
|
||||
store pluginstore.Store
|
||||
pluginAssets PluginAssetsCalculator
|
||||
}
|
||||
|
||||
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
|
||||
// pluginAssets is required for calculating loading strategy and module hash.
|
||||
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
|
||||
return &LocalProvider{
|
||||
store: pluginStore,
|
||||
pluginAssets: pluginAssets,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMeta retrieves plugin metadata for locally installed plugins.
|
||||
func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
plugin, exists := p.store.Plugin(ctx, pluginID)
|
||||
if !exists {
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
|
||||
moduleHash := p.pluginAssets.ModuleHash(ctx, plugin)
|
||||
|
||||
spec := pluginStorePluginToPluginMetaSpec(plugin, loadingStrategy, moduleHash)
|
||||
return &Result{
|
||||
Meta: spec,
|
||||
TTL: defaultLocalTTL,
|
||||
}, nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
|
||||
// cachedMeta represents a cached metadata entry with expiration time
|
||||
type cachedMeta struct {
|
||||
meta pluginsv0alpha1.PluginMetaJSONData
|
||||
meta pluginsv0alpha1.PluginMetaSpec
|
||||
ttl time.Duration
|
||||
expiresAt time.Time
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string
|
||||
if err == nil {
|
||||
// Don't cache results with a zero TTL
|
||||
if result.TTL == 0 {
|
||||
continue
|
||||
return result, nil
|
||||
}
|
||||
|
||||
pm.cacheMu.Lock()
|
||||
|
||||
@@ -35,10 +35,12 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns cached result when available and not expired", func(t *testing.T) {
|
||||
cachedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
cachedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider := &mockProvider{
|
||||
@@ -60,8 +62,10 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
|
||||
provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"},
|
||||
TTL: time.Hour,
|
||||
Meta: pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "different"},
|
||||
},
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -73,10 +77,12 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("fetches from provider when not cached", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
expectedTTL := 2 * time.Hour
|
||||
|
||||
@@ -107,19 +113,16 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
assert.Equal(t, expectedTTL, cached.ttl)
|
||||
})
|
||||
|
||||
t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) {
|
||||
zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Zero TTL Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
t.Run("does not cache result with zero TTL", func(t *testing.T) {
|
||||
zeroTTLMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Zero TTL Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: zeroTTLMeta,
|
||||
@@ -127,37 +130,30 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
pm := NewProviderManager(provider)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, zeroTTLMeta, result.Meta)
|
||||
assert.Equal(t, time.Duration(0), result.TTL)
|
||||
|
||||
pm.cacheMu.RLock()
|
||||
cached, exists := pm.cache["test-plugin:1.0.0"]
|
||||
_, exists := pm.cache["test-plugin:1.0.0"]
|
||||
pm.cacheMu.RUnlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, expectedMeta, cached.meta)
|
||||
assert.Equal(t, time.Hour, cached.ttl)
|
||||
assert.False(t, exists, "zero TTL results should not be cached")
|
||||
})
|
||||
|
||||
t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
@@ -229,15 +225,19 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("skips expired cache entries", func(t *testing.T) {
|
||||
expiredMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Expired Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expiredMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Expired Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
@@ -272,15 +272,19 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uses first successful provider", func(t *testing.T) {
|
||||
expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 1 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedMeta1 := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 1 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 2 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
expectedMeta2 := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 2 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
@@ -331,9 +335,9 @@ func TestProviderManager_Run(t *testing.T) {
|
||||
|
||||
func TestProviderManager_cleanupExpired(t *testing.T) {
|
||||
t.Run("removes expired entries", func(t *testing.T) {
|
||||
validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"}
|
||||
expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"}
|
||||
expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"}
|
||||
validMeta := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "valid"}}
|
||||
expiredMeta1 := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"}}
|
||||
expiredMeta2 := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"}}
|
||||
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
|
||||
@@ -14,14 +14,14 @@ var (
|
||||
|
||||
// Result contains plugin metadata along with its recommended TTL.
|
||||
type Result struct {
|
||||
Meta pluginsv0alpha1.PluginMetaJSONData
|
||||
Meta pluginsv0alpha1.PluginMetaSpec
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// Provider is used for retrieving plugin metadata.
|
||||
type Provider interface {
|
||||
// GetMeta retrieves plugin metadata for the given plugin ID and version.
|
||||
// Returns the Result containing the PluginMetaJSONData and its recommended TTL.
|
||||
// Returns the Result containing the PluginMetaSpec and its recommended TTL.
|
||||
// If the plugin is not found, returns ErrMetaNotFound.
|
||||
GetMeta(ctx context.Context, pluginID, version string) (*Result, error)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L
|
||||
continue
|
||||
}
|
||||
|
||||
pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace)
|
||||
pluginMeta := createPluginMetaFromSpec(result.Meta, plugin.Name, plugin.Namespace)
|
||||
metaItems = append(metaItems, *pluginMeta)
|
||||
}
|
||||
|
||||
@@ -174,19 +174,17 @@ func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err))
|
||||
}
|
||||
|
||||
return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil
|
||||
return createPluginMetaFromSpec(result.Meta, name, ns.Value), nil
|
||||
}
|
||||
|
||||
// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata.
|
||||
func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta {
|
||||
// createPluginMetaFromSpec creates a PluginMeta k8s object from PluginMetaSpec and plugin metadata.
|
||||
func createPluginMetaFromSpec(spec pluginsv0alpha1.PluginMetaSpec, name, namespace string) *pluginsv0alpha1.PluginMeta {
|
||||
pluginMeta := &pluginsv0alpha1.PluginMeta{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJSON: pluginJSON,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
// Set the GroupVersionKind
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/apiserver v0.34.2
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
|
||||
)
|
||||
@@ -90,7 +91,6 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.34.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.2 // indirect
|
||||
k8s.io/apiserver v0.34.2 // indirect
|
||||
k8s.io/client-go v0.34.2 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func GetAuthorizer() authorizer.Authorizer {
|
||||
return authorizer.AuthorizerFunc(func(
|
||||
ctx context.Context, attr authorizer.Attributes,
|
||||
) (authorized authorizer.Decision, reason string, err error) {
|
||||
if !attr.IsResourceRequest() {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
// Any authenticated user can access the API
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
}
|
||||
@@ -14,10 +14,10 @@ weight: 400
|
||||
The Grafana Cloud Migration Assistant, generally available from Grafana v12.0, automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionality:
|
||||
|
||||
- Securely connect your self-managed instance to a Grafana Cloud instance.
|
||||
- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
|
||||
- Migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
|
||||
- View the migration status of your resources in real-time.
|
||||
|
||||
Some of the benefits of the migration assistant are:
|
||||
Some benefits of the migration assistant are:
|
||||
|
||||
Ease of use
|
||||
: Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts.
|
||||
@@ -44,7 +44,7 @@ The following resources are supported by the migration assistant:
|
||||
|
||||
To use the Grafana migration assistant, you need:
|
||||
|
||||
- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
|
||||
- A self-managed Grafana instance version v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
|
||||
- A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to.
|
||||
- [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs/<YOUR-ORG-NAME>/members`.
|
||||
- [Grafana server administrator](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https://<GRAFANA-ONPREM-URL>/admin/users`.
|
||||
@@ -64,7 +64,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi
|
||||
|
||||
### Grant access in Grafana Enterprise
|
||||
|
||||
{{< admonition type="important">}}
|
||||
{{< admonition type="note" >}}
|
||||
You must [configure RBAC](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant.
|
||||
{{< /admonition >}}
|
||||
|
||||
|
||||
@@ -74,6 +74,21 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
configure-grafana-azure-auth:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
configure-grafana-azure:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
configure-grafana-azure-auth-scopes:
|
||||
- pattern: /docs/grafana/
|
||||
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
|
||||
---
|
||||
|
||||
# Configure the Microsoft SQL Server data source
|
||||
@@ -138,14 +153,19 @@ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2, y
|
||||
|
||||
**Authentication:**
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
{{< admonition type="note" >}}
|
||||
In order to use Azure AD Authentication the toggle `auth.azure_auth_enabled` must be set to `true` in the Grafana configuration file.
|
||||
{{< /admonition >}}
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
| **Azure Entra ID (formerly Azure AD) Authentication** | Authenticates the data source using Azure authentication methods. | Details on the supported authentication methods and how to configure them can be found in the [Azure authentication section](./index.md#azure-entra-id-formerly-azure-ad-authentication). |
|
||||
|
||||
**Additional settings:**
|
||||
|
||||
@@ -185,6 +205,123 @@ After configuring your MSSQL data source options, click **Save & test** at the b
|
||||
|
||||
**Database Connection OK**
|
||||
|
||||
### Azure Entra ID (formerly Azure AD) Authentication
|
||||
|
||||
The following Azure authentication methods are supported:
|
||||
|
||||
- Current User authentication
|
||||
- App Registration
|
||||
- Managed Identity
|
||||
- Azure Entra Password
|
||||
|
||||
The Azure SQL Server that you are connecting to should support Azure Entra authentication to support adding the App Registration as a user in the database. For configuration details, refer to the [Azure SQL documentation](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-portal).
|
||||
|
||||
#### Current User authentication
|
||||
|
||||
This is the recommended authentication mechanism when working with SQL Server instances that are hosted in Azure. It allows users to be authenticated to and query the database using their own credentials rather than long-lived credentials.
|
||||
|
||||
This authentication method requires your Grafana instance to be configured with Azure Entra ID (formerly Active Directory) authentication for login. With Azure Entra ID login, this method can be used to forward the currently logged in user’s credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation](ref:configure-grafana-azure-auth).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source.
|
||||
|
||||
The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**.
|
||||
|
||||
1. In the Azure Portal, open the App Registration that requires configuration.
|
||||
2. Select **Authentication** in the side menu.
|
||||
3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes.
|
||||
4. Save the changes to ensure the App Registration is updated.
|
||||
|
||||
The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source.
|
||||
|
||||
1. In the Azure Portal, open the App Registration that requires configuration.
|
||||
1. Select **API Permissions** in the side menu.
|
||||
1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added.
|
||||
1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information.
|
||||
- Select **APIs my organization uses** > Search for **Azure SQL** and select it > **Delegated permissions** > `user_impersonation` > **Add permissions**
|
||||
|
||||
After all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. After updated the scopes value should equal: `.default openid email profile`.
|
||||
{{< /admonition >}}
|
||||
|
||||
This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. Affected functionality includes alerting, reporting, and recorded queries. Also, note that query and resource caching is disabled by default for data sources using current user authentication.
|
||||
|
||||
**To enable current user authentication for Grafana:**
|
||||
|
||||
1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
user_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Current User**.
|
||||
|
||||
### App Registration
|
||||
|
||||
You must create an app registration and service principal in Azure Entra to authenticate the data source.
|
||||
For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in).
|
||||
|
||||
After the app registration has been created, make note of the tenant ID, client ID, and client secret. Take the following steps to add the app registration as a SQL user:
|
||||
|
||||
1. Connect to your Azure SQL database as a user with administrative permissions (the user used here must have the ability to read your Azure Entra directory e.g. by possessing the `Directory Readers` role).
|
||||
2. Run `CREATE USER [$IDENTITY_NAME] FROM EXTERNAL PROVIDER;`, substituting `IDENTITY_NAME` with the app registration name.
|
||||
3. Grant the created user the appropriate level of permissions for your use-case. It is recommended that users configured for data sources only have reader permissions.
|
||||
|
||||
After the appropriate permissions have been granted, configure the SQL Server data source to use the app registration:
|
||||
|
||||
1. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **App Registration**.
|
||||
2. Set the **Azure Cloud** value to the correct value. If you are using the Azure public cloud this will be **Azure**.
|
||||
3. Set the **Directory (tenant) ID**, **Application (client) ID**, and **Client Secret** values to those for your app registration.
|
||||
|
||||
### Managed Identity
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can use managed identity to configure SQL Server in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM.
|
||||
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations.
|
||||
For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview).
|
||||
|
||||
**To enable managed identity for Grafana:**
|
||||
|
||||
1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
managed_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Managed Identity**.
|
||||
|
||||
This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to SQL Server.
|
||||
|
||||
3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity.
|
||||
|
||||
Ensure that the managed identity used is added to your Azure SQL instance as a user.
|
||||
|
||||
### Azure Entra Password
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
Azure Entra Password is not a recommended authentication mechanism as it requires configuration using a single users password. Consider an alternative authentication method such as current user authentication or app registration.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can connect to an Azure SQL database using the username and password of a user that has permissions in the desired database. This also requires an app registration to be configured with access to the database.
|
||||
|
||||
**To enable Azure Entra password for Grafana:**
|
||||
|
||||
1. Set the `azure_entra_password_credentials_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
azure_entra_password_credentials_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Azure Entra Password**.
|
||||
3. Set the **User ID** value to the username of the user in the Azure SQL database.
|
||||
4. Set the **Application Client ID** to the client ID of the app registration that has been added to the Azure SQL database
|
||||
5. Set the **Password** value to the password of the user in the Azure SQL database.
|
||||
|
||||
### Min time interval
|
||||
|
||||
The **Min time interval** setting defines a lower limit for the [`$__interval`](ref:add-template-variables-interval) and [`$__interval_ms`][add-template-variables-interval_ms] variables.
|
||||
|
||||
@@ -767,12 +767,12 @@ Status Codes:
|
||||
|
||||
Deletes a dashboard via the dashboard uid.
|
||||
|
||||
- namespace: to read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/developers/http_api/apis/).
|
||||
- uid: the unique identifier of the dashboard to update. this will be the _name_ in the dashboard response
|
||||
- **`namespace`**: To read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/developers/http_api/apis/).
|
||||
- **`uid`**: The unique identifier of the dashboard to update. This is the `metadata.name` field in the dashboard response and _not_ the `metadata.uid` field.
|
||||
|
||||
**Required permissions**
|
||||
|
||||
See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation.
|
||||
See note in the [introduction](#new-dashboard-apis) for an explanation.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
| Action | Scope |
|
||||
|
||||
@@ -1933,6 +1933,10 @@ The initial delay before retrying a failed alert evaluation. Default is `1s`.
|
||||
|
||||
This value is the starting point for exponential backoff.
|
||||
|
||||
#### `initialization_timeout`
|
||||
|
||||
Allows the context deadline for the `AlertNG` service to be configurable. The default timeout is 30s.
|
||||
|
||||
#### `max_retry_delay`
|
||||
|
||||
The maximum delay between retries during exponential backoff. Default is `4s`.
|
||||
|
||||
@@ -66,6 +66,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes |
|
||||
| `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 | |
|
||||
|
||||
## Public preview feature toggles
|
||||
@@ -95,9 +96,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown |
|
||||
| `logsPanelControls` | Enables a control component for the logs panel in Explore |
|
||||
| `interactiveLearning` | Enables the interactive learning app |
|
||||
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker |
|
||||
| `newVizSuggestions` | Enable new visualization suggestions |
|
||||
| `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; |
|
||||
| `newPanelPadding` | Increases panel padding globally |
|
||||
| `transformationsEmptyPlaceholder` | Show transformation quick-start cards in empty transformations state |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
@@ -414,13 +414,13 @@ test.describe(
|
||||
).toBeVisible();
|
||||
|
||||
// Go back to dashboard options
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
// Expand layouts section
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select tabs layout
|
||||
await page.getByLabel('Tabs').click();
|
||||
await page.getByLabel('layout-selection-option-Tabs').click();
|
||||
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
|
||||
@@ -518,14 +518,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('Test row 1');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 1')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name
|
||||
await expect(
|
||||
@@ -543,14 +543,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('Test row 2');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 2')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name + 1 to avoid duplicates
|
||||
await expect(
|
||||
@@ -755,13 +755,13 @@ test.describe(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
|
||||
|
||||
// Go back to dashboard options
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
// Expand layouts section
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select rows layout
|
||||
await page.getByLabel('Rows').click();
|
||||
await page.getByLabel('layout-selection-option-Rows').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1'))
|
||||
@@ -903,14 +903,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('Test tab 1');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 1')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
|
||||
@@ -923,14 +923,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('Test tab 2');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 2')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name + 1 to avoid duplicates
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
|
||||
|
||||
@@ -21,6 +21,7 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
|
||||
// Should be able to click Variables item in outline to see add variable button
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
||||
@@ -28,6 +29,8 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
|
||||
// Clicking a panel should scroll that panel in view
|
||||
await expect(page.getByText('Dashboard panel 48')).toBeHidden();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click();
|
||||
|
||||
@@ -22,6 +22,9 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
const undockButton = page.getByRole('button', { name: 'Undock menu' });
|
||||
await undockButton.click();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -199,6 +199,7 @@ test.describe(
|
||||
.click();
|
||||
|
||||
// Open the modal editor in the side pane
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click();
|
||||
await openModal(dashboardPage, selectors);
|
||||
|
||||
@@ -32,9 +32,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
@@ -50,6 +50,7 @@ test.describe(
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await checkAutoGridLayoutInputs(dashboardPage, selectors);
|
||||
});
|
||||
@@ -63,9 +64,10 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
// Get initial positions - standard width should have panels on different rows
|
||||
const firstPanelTop = await getPanelTop(dashboardPage, selectors);
|
||||
@@ -98,6 +100,7 @@ test.describe(
|
||||
await page.reload();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
@@ -123,9 +126,10 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
|
||||
@@ -134,7 +138,7 @@ test.describe(
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
|
||||
.fill('900');
|
||||
.fill('1100');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
|
||||
.blur();
|
||||
@@ -148,12 +152,13 @@ test.describe(
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth
|
||||
)
|
||||
).toHaveValue('900');
|
||||
).toHaveValue('1100');
|
||||
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
@@ -180,9 +185,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
|
||||
@@ -198,6 +203,7 @@ test.describe(
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
|
||||
@@ -215,9 +221,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
|
||||
|
||||
@@ -250,6 +256,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
|
||||
@@ -270,9 +277,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
|
||||
|
||||
@@ -303,6 +310,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
@@ -327,9 +335,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
// Set narrow column width first to ensure panels fit horizontally
|
||||
await dashboardPage
|
||||
@@ -357,6 +365,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
|
||||
|
||||
@@ -37,9 +37,10 @@ test.describe(
|
||||
},
|
||||
() => {
|
||||
test('can enable repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(page, selectors, 'Auto grid repeats - add repeats');
|
||||
await importTestDashboard(page, selectors, 'Auto-grid repeats - add repeats');
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -70,11 +71,12 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update on variable change',
|
||||
'Auto-grid repeats - update on variable change',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
@@ -113,6 +115,7 @@ test.describe(
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -138,11 +141,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update through panel editor',
|
||||
'Auto-grid repeats - update through panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -202,11 +207,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update through directly loaded panel editor',
|
||||
'Auto-grid repeats - update through directly loaded panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
|
||||
@@ -257,11 +264,12 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - move repeated panels',
|
||||
'Auto-grid repeats - move repeated panels',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -304,11 +312,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - move repeated panels',
|
||||
'Auto-grid repeats - move repeated panels 2',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -332,9 +342,7 @@ test.describe(
|
||||
|
||||
const repeatedPanelUrl = page.url();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`))
|
||||
@@ -367,11 +375,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - view embedded repeated panel',
|
||||
'Auto-grid repeats - view embedded repeated panel',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -393,11 +403,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - remove repeats',
|
||||
'Auto-grid repeats - remove repeats',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -453,5 +465,5 @@ test.describe(
|
||||
|
||||
async function switchToAutoGrid(page: Page) {
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
}
|
||||
|
||||
@@ -303,9 +303,7 @@ test.describe(
|
||||
|
||||
const repeatedPanelUrl = page.url();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`))
|
||||
|
||||
@@ -316,9 +316,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// repeated panel in original tab repeat
|
||||
await dashboardPage
|
||||
@@ -341,9 +339,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2'))
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// repeated panel in repeated tab
|
||||
await dashboardPage
|
||||
|
||||
@@ -21,11 +21,7 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
// Check that current dashboard title is visible in breadcrumb
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering'))
|
||||
).toBeVisible();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input');
|
||||
await expect(titleInput).toHaveValue('Annotation filtering');
|
||||
|
||||
@@ -48,6 +48,7 @@ export const flows = {
|
||||
},
|
||||
async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
||||
|
||||
@@ -1907,21 +1907,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
|
||||
@@ -104,7 +104,7 @@ require (
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend
|
||||
github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs
|
||||
github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team
|
||||
|
||||
@@ -1647,8 +1647,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg
|
||||
github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
|
||||
@@ -896,6 +896,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA=
|
||||
github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
|
||||
github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A=
|
||||
@@ -1323,6 +1324,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
|
||||
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
|
||||
github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=
|
||||
@@ -1862,6 +1864,7 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
@@ -1890,6 +1893,7 @@ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
@@ -1912,6 +1916,7 @@ golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
@@ -1920,6 +1925,7 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
@@ -1948,6 +1954,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1967,6 +1974,7 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
@@ -1989,6 +1997,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
|
||||
+2
-2
@@ -612,8 +612,8 @@ export type GetSearchApiArg = {
|
||||
tags?: string[];
|
||||
/** find dashboards that reference a given libraryPanel */
|
||||
libraryPanel?: string;
|
||||
/** permission needed for the resource (View, Edit, Admin) */
|
||||
permission?: 'View' | 'Edit' | 'Admin';
|
||||
/** permission needed for the resource (view, edit, admin) */
|
||||
permission?: 'view' | 'edit' | 'admin';
|
||||
/** sortable field */
|
||||
sort?: string;
|
||||
/** number of results to return */
|
||||
|
||||
@@ -561,6 +561,10 @@ const injectedRtkApi = api
|
||||
}),
|
||||
invalidatesTags: ['Team'],
|
||||
}),
|
||||
getTeamGroups: build.query<GetTeamGroupsApiResponse, GetTeamGroupsApiArg>({
|
||||
query: (queryArg) => ({ url: `/teams/${queryArg.name}/groups` }),
|
||||
providesTags: ['Team'],
|
||||
}),
|
||||
getTeamMembers: build.query<GetTeamMembersApiResponse, GetTeamMembersApiArg>({
|
||||
query: (queryArg) => ({ url: `/teams/${queryArg.name}/members` }),
|
||||
providesTags: ['Team'],
|
||||
@@ -1461,6 +1465,11 @@ export type UpdateTeamApiArg = {
|
||||
force?: boolean;
|
||||
patch: Patch;
|
||||
};
|
||||
export type GetTeamGroupsApiResponse = /** status 200 OK */ GetGroups;
|
||||
export type GetTeamGroupsApiArg = {
|
||||
/** name of the GetGroups */
|
||||
name: string;
|
||||
};
|
||||
export type GetTeamMembersApiResponse = /** status 200 OK */ TeamMemberList;
|
||||
export type GetTeamMembersApiArg = {
|
||||
/** name of the TeamMemberList */
|
||||
@@ -1978,6 +1987,17 @@ export type TeamList = {
|
||||
kind?: string;
|
||||
metadata: ListMeta;
|
||||
};
|
||||
export type VersionsV0Alpha1Kinds7RoutesGroupsGetResponseExternalGroupMapping = {
|
||||
externalGroup: string;
|
||||
name: string;
|
||||
};
|
||||
export type GetGroups = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion?: string;
|
||||
items: VersionsV0Alpha1Kinds7RoutesGroupsGetResponseExternalGroupMapping[];
|
||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||
kind?: string;
|
||||
};
|
||||
export type TeamMember = {
|
||||
/** AvatarURL is the url where we can get the avatar for identity */
|
||||
avatarURL?: string;
|
||||
@@ -2100,6 +2120,8 @@ export const {
|
||||
useReplaceTeamMutation,
|
||||
useDeleteTeamMutation,
|
||||
useUpdateTeamMutation,
|
||||
useGetTeamGroupsQuery,
|
||||
useLazyGetTeamGroupsQuery,
|
||||
useGetTeamMembersQuery,
|
||||
useLazyGetTeamMembersQuery,
|
||||
useListUserQuery,
|
||||
|
||||
@@ -6039,6 +6039,7 @@ export type TeamGroupDto = {
|
||||
groupId?: string;
|
||||
orgId?: number;
|
||||
teamId?: number;
|
||||
uid?: string;
|
||||
};
|
||||
export type TeamGroupMapping = {
|
||||
groupId?: string;
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/plugin-types": "^0.0.48",
|
||||
"@grafana/scenes": "6.38.0",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
|
||||
@@ -921,3 +921,4 @@ export {
|
||||
} from './rbac/rbac';
|
||||
|
||||
export { type UserStorage } from './types/userStorage';
|
||||
export { type PluginMetasResponse, type PluginMetasSpec } from './types/plugin';
|
||||
|
||||
@@ -381,6 +381,11 @@ export class PanelPlugin<
|
||||
const appender = builder.getListAppender<TOptions, TFieldConfigOptions>({
|
||||
pluginId: this.meta.id,
|
||||
name: this.meta.name,
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = supplier(builder.dataSummary);
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { PreferredVisualisationType } from '../../types/data';
|
||||
import { DataFrame, FieldType } from '../../types/dataFrame';
|
||||
import { DataFrameType } from '../../types/dataFrameTypes';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface PanelDataSummary {
|
||||
hasData?: boolean;
|
||||
rowCountTotal: number;
|
||||
/** max number of rows in any given dataframe in the panel data */
|
||||
rowCountMax: number;
|
||||
frameCount: number;
|
||||
fieldCount: number;
|
||||
/** max number of fields in any given dataframe in the panel data */
|
||||
fieldCountMax: number;
|
||||
/** given a field type, return the number of fields across all dataframes which match this type */
|
||||
fieldCountByType: (type: FieldType) => number;
|
||||
/** returns true if any fields in any frames match the field type */
|
||||
hasFieldType: (type: FieldType) => boolean;
|
||||
/** The first frame that set's this value */
|
||||
preferredVisualisationType?: PreferredVisualisationType;
|
||||
/* returns true if any of the frames in this panel data summary have the type */
|
||||
hasDataFrameType: (type: DataFrameType) => boolean;
|
||||
/* returns true if any of the frames in this panel data summary have the type */
|
||||
hasPreferredVisualisationType: (type: PreferredVisualisationType) => boolean;
|
||||
|
||||
/** pass along a reference to the DataFrame array in case it's needed by the plugin */
|
||||
rawFrames?: DataFrame[];
|
||||
|
||||
/* --- DEPRECATED FIELDS BELOW --- */
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
|
||||
@@ -23,60 +31,114 @@ export interface PanelDataSummary {
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
|
||||
stringFieldCount: number;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
|
||||
hasNumberField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasTimeField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasNumberField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
|
||||
hasStringField?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
class PanelDataSummaryImpl implements PanelDataSummary {
|
||||
public rowCountTotal = 0;
|
||||
/** max number of rows in any single dataframe in the panel data */
|
||||
public rowCountMax = 0;
|
||||
public fieldCount = 0;
|
||||
/** max number of fields in any single dataframe in the panel data */
|
||||
public fieldCountMax = 0;
|
||||
|
||||
private countByType: Partial<Record<FieldType, number>> = {};
|
||||
private preferredVisualisationTypes: Set<PreferredVisualisationType> = new Set<PreferredVisualisationType>();
|
||||
private dataFrameTypes: Set<DataFrameType> = new Set<DataFrameType>();
|
||||
|
||||
public get hasData(): boolean {
|
||||
return this.rowCountTotal > 0;
|
||||
}
|
||||
|
||||
public get frameCount(): number {
|
||||
return this.rawFrames?.length ?? 0;
|
||||
}
|
||||
|
||||
constructor(public rawFrames?: DataFrame[]) {
|
||||
this._processFrames();
|
||||
}
|
||||
|
||||
private _processFrames() {
|
||||
for (const frame of this.rawFrames ?? []) {
|
||||
this.rowCountTotal += frame.length;
|
||||
|
||||
if (frame.meta?.preferredVisualisationType) {
|
||||
this.preferredVisualisationTypes.add(frame.meta.preferredVisualisationType);
|
||||
}
|
||||
if (frame.meta?.type) {
|
||||
this.dataFrameTypes.add(frame.meta.type);
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
this.fieldCount++;
|
||||
this.countByType[field.type] = (this.countByType[field.type] || 0) + 1;
|
||||
}
|
||||
|
||||
if (frame.length > this.rowCountMax) {
|
||||
this.rowCountMax = frame.length;
|
||||
}
|
||||
if (frame.fields.length > this.fieldCountMax) {
|
||||
this.fieldCountMax = frame.fields.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fieldCountByType(type: FieldType): number {
|
||||
return this.countByType[type] ?? 0;
|
||||
}
|
||||
|
||||
public hasFieldType(type: FieldType): boolean {
|
||||
return this.fieldCountByType(type) > 0;
|
||||
}
|
||||
|
||||
public hasPreferredVisualisationType(type: PreferredVisualisationType): boolean {
|
||||
return this.preferredVisualisationTypes.has(type);
|
||||
}
|
||||
|
||||
public hasDataFrameType(type: DataFrameType): boolean {
|
||||
return this.dataFrameTypes.has(type);
|
||||
}
|
||||
|
||||
/**** DEPRECATED ****/
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
|
||||
public get numberFieldCount(): number {
|
||||
return this.fieldCountByType(FieldType.number);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */
|
||||
public get timeFieldCount(): number {
|
||||
return this.fieldCountByType(FieldType.time);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
|
||||
public get stringFieldCount() {
|
||||
return this.fieldCountByType(FieldType.string);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
|
||||
public get hasTimeField() {
|
||||
return this.fieldCountByType(FieldType.time) > 0;
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
public get hasNumberField() {
|
||||
return this.fieldCountByType(FieldType.number) > 0;
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
|
||||
public get hasStringField() {
|
||||
return this.fieldCountByType(FieldType.string) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* given a list of dataframes, summarize attributes of those frames for features like suggestions.
|
||||
* @param frames - dataframes to summarize
|
||||
* @returns summary of the dataframes
|
||||
*/
|
||||
export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary {
|
||||
let rowCountTotal = 0;
|
||||
let rowCountMax = 0;
|
||||
let fieldCount = 0;
|
||||
const countByType: Partial<Record<FieldType, number>> = {};
|
||||
let preferredVisualisationType: PreferredVisualisationType | undefined;
|
||||
|
||||
for (const frame of frames) {
|
||||
rowCountTotal += frame.length;
|
||||
|
||||
if (frame.meta?.preferredVisualisationType) {
|
||||
preferredVisualisationType = frame.meta.preferredVisualisationType;
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
fieldCount++;
|
||||
countByType[field.type] = (countByType[field.type] || 0) + 1;
|
||||
}
|
||||
|
||||
if (frame.length > rowCountMax) {
|
||||
rowCountMax = frame.length;
|
||||
}
|
||||
}
|
||||
|
||||
const fieldCountByType = (f: FieldType) => countByType[f] ?? 0;
|
||||
|
||||
return {
|
||||
rowCountTotal,
|
||||
rowCountMax,
|
||||
fieldCount,
|
||||
preferredVisualisationType,
|
||||
frameCount: frames.length,
|
||||
hasData: rowCountTotal > 0,
|
||||
hasFieldType: (f: FieldType) => fieldCountByType(f) > 0,
|
||||
fieldCountByType,
|
||||
// deprecated
|
||||
numberFieldCount: fieldCountByType(FieldType.number),
|
||||
timeFieldCount: fieldCountByType(FieldType.time),
|
||||
stringFieldCount: fieldCountByType(FieldType.string),
|
||||
hasTimeField: fieldCountByType(FieldType.time) > 0,
|
||||
hasNumberField: fieldCountByType(FieldType.number) > 0,
|
||||
hasStringField: fieldCountByType(FieldType.string) > 0,
|
||||
};
|
||||
export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary {
|
||||
return new PanelDataSummaryImpl(frames);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ interface IndexOptions {
|
||||
asPercentile: boolean;
|
||||
}
|
||||
|
||||
const defaultReduceOptions: ReduceOptions = {
|
||||
const defaultNumericVizOptions: ReduceOptions = {
|
||||
reducer: ReducerID.sum,
|
||||
};
|
||||
|
||||
@@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
|
||||
switch (mode) {
|
||||
case CalculateFieldMode.ReduceRow:
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultNumericVizOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.CumulativeFunctions:
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultNumericVizOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.WindowFunctions:
|
||||
creator = getWindowCreator(defaults(options.window, defaultWindowOptions), data);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { DataQuery, LogsSortOrder } from '@grafana/schema';
|
||||
|
||||
import { PreferredVisualisationType } from './data';
|
||||
import { SelectableValue } from './select';
|
||||
@@ -84,6 +84,7 @@ export interface ExploreLogsPanelState {
|
||||
// Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized
|
||||
refId?: string;
|
||||
displayedFields?: string[];
|
||||
sortOrder?: LogsSortOrder;
|
||||
}
|
||||
|
||||
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
||||
|
||||
+6
-2
@@ -963,6 +963,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
kubernetesAuthnMutation?: boolean;
|
||||
/**
|
||||
* Routes external group mapping requests from /api to the /apis endpoint
|
||||
*/
|
||||
kubernetesExternalGroupMapping?: boolean;
|
||||
/**
|
||||
* Enables restore deleted dashboards feature
|
||||
* @default false
|
||||
*/
|
||||
@@ -1087,7 +1091,7 @@ export interface FeatureToggles {
|
||||
graphiteBackendMode?: boolean;
|
||||
/**
|
||||
* Enables the updated Azure Monitor resource picker
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
azureResourcePickerUpdates?: boolean;
|
||||
/**
|
||||
@@ -1146,7 +1150,7 @@ export interface FeatureToggles {
|
||||
pluginStoreServiceLoading?: boolean;
|
||||
/**
|
||||
* Increases panel padding globally
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
newPanelPadding?: boolean;
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { PluginSchema } from '@grafana/plugin-types/plugin-schema';
|
||||
|
||||
import { KeyValue } from './data';
|
||||
import { IconName } from './icon';
|
||||
|
||||
@@ -266,3 +268,32 @@ export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
|
||||
this.meta = {} as T;
|
||||
}
|
||||
}
|
||||
export interface PluginMetasResponse {
|
||||
items: PluginMetasItem[];
|
||||
}
|
||||
|
||||
export interface PluginMetasItem {
|
||||
spec: PluginMetasSpec;
|
||||
}
|
||||
|
||||
export interface PluginMetasSpec {
|
||||
pluginJson: PluginSchema;
|
||||
module: PluginMetasModule;
|
||||
baseURL: string;
|
||||
signature: PluginMetasSignature;
|
||||
angular: PluginMetasAngular;
|
||||
translations?: PluginSchema['languages'];
|
||||
}
|
||||
|
||||
export interface PluginMetasAngular {
|
||||
detected: boolean;
|
||||
}
|
||||
|
||||
export interface PluginMetasModule {
|
||||
path: string;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
}
|
||||
|
||||
export interface PluginMetasSignature {
|
||||
status: PluginSignatureStatus;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ export interface VisualizationSuggestion<TOptions extends unknown = {}, TFieldCo
|
||||
* mutate the suggestion object which is passed in as the first argument.
|
||||
*/
|
||||
previewModifier?: (suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) => void;
|
||||
/** @deprecated this will no longer be supported in the new Suggestions UI. */
|
||||
icon?: string;
|
||||
/** @deprecated this will no longer be supported in the new Suggestions UI. */
|
||||
imgSrc?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const components = {
|
||||
|
||||
A few things to keep in mind:
|
||||
|
||||
- Strive to use e2e selector for all components in grafana/ui.
|
||||
- Strive to use e2e selectors for all components in grafana/ui.
|
||||
- Don't ever delete selectors. Even though a selector may not be used in the Grafana repository, it can still be used in external plugins.
|
||||
- Only create new selector in case you're creating a new piece of UI. If you're changing an existing piece of UI that already has a selector defined, you need to keep using that selector. Otherwise you might break plugin end-to-end tests.
|
||||
- Prefer using string selectors in favour of function selectors. The purpose of the selectors is to provide a canonical way to select elements.
|
||||
|
||||
@@ -57,6 +57,11 @@ export const versionedComponents = {
|
||||
'12.1.0': 'data-testid DashboardEditPaneSplitter primary body',
|
||||
},
|
||||
},
|
||||
Sidebar: {
|
||||
closePane: {
|
||||
'12.4.0': 'data-testid Sidebar close pane',
|
||||
},
|
||||
},
|
||||
EditPaneHeader: {
|
||||
deleteButton: {
|
||||
'12.1.0': 'data-testid EditPaneHeader delete panel',
|
||||
@@ -70,9 +75,6 @@ export const versionedComponents = {
|
||||
duplicate: {
|
||||
'12.1.0': 'data-testid EditPaneHeader duplicate',
|
||||
},
|
||||
backButton: {
|
||||
'12.1.0': 'data-testid EditPaneHeader back',
|
||||
},
|
||||
},
|
||||
TimePicker: {
|
||||
openButton: {
|
||||
|
||||
@@ -183,6 +183,14 @@ export const versionedPages = {
|
||||
url: {
|
||||
[MIN_GRAFANA_VERSION]: (uid: string) => `/d/${uid}`,
|
||||
},
|
||||
Sidebar: {
|
||||
optionsButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar options button',
|
||||
},
|
||||
outlineButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar outline button',
|
||||
},
|
||||
},
|
||||
DashNav: {
|
||||
nav: {
|
||||
[MIN_GRAFANA_VERSION]: 'Dashboard navigation',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// This script is used to copy assets from the public folder to a temporary static folder within
|
||||
// the .storybook directory.
|
||||
// We selectively limit assets that are uploaded to the Storybook bucket to prevent rate limiting
|
||||
// when publishing new storybook.
|
||||
// Note: Storybook has a static copying feature but it copies entire directories which can contain thousands of icons.
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// avoid importing from @grafana/data to prevent error: window is not defined
|
||||
import { availableIconsIndex, IconName } from '../../grafana-data/src/types/icon.ts';
|
||||
import { getIconSubDir } from '../../grafana-ui/src/components/Icon/utils.ts';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
// doesn't require uploading 1000s of unused assets.
|
||||
const iconPaths = Object.keys(availableIconsIndex)
|
||||
.filter((iconName) => !iconName.startsWith('fa '))
|
||||
.map((iconName) => {
|
||||
const subDir = getIconSubDir(iconName as IconName, 'default');
|
||||
return {
|
||||
from: `../../../public/img/icons/${subDir}/${iconName}.svg`,
|
||||
to: `./static/public/build/img/icons/${subDir}/${iconName}.svg`,
|
||||
};
|
||||
});
|
||||
|
||||
export function copyAssetsSync() {
|
||||
const assets = [
|
||||
{
|
||||
from: '../../../public/fonts',
|
||||
to: './static/public/fonts',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/grafana_text_logo-dark.svg',
|
||||
to: './static/public/img/grafana_text_logo-dark.svg',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/grafana_text_logo-light.svg',
|
||||
to: './static/public/img/grafana_text_logo-light.svg',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/fav32.png',
|
||||
to: './static/public/img/fav32.png',
|
||||
},
|
||||
{
|
||||
from: '../../../public/lib',
|
||||
to: './static/public/lib',
|
||||
},
|
||||
...iconPaths,
|
||||
// copy over the MSW mock service worker so we can mock requests in Storybook
|
||||
{
|
||||
from: '../../../public/mockServiceWorker.js',
|
||||
to: './static/mockServiceWorker.js',
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
const fromPath = resolve(__dirname, asset.from);
|
||||
const toPath = resolve(__dirname, asset.to);
|
||||
if (!fs.existsSync(toPath)) {
|
||||
fs.copySync(fromPath, toPath, {
|
||||
filter: (src) => !fs.lstatSync(src).isSymbolicLink(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
import { copyAssetsSync } from './copyAssets.ts';
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
copyAssetsSync();
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.story.tsx'],
|
||||
addons: [
|
||||
{
|
||||
name: '@storybook/preset-scss',
|
||||
options: {
|
||||
styleLoaderOptions: {
|
||||
// this is required for theme switching .use() and .unuse()
|
||||
injectType: 'lazyStyleTag',
|
||||
},
|
||||
cssLoaderOptions: {
|
||||
url: false,
|
||||
importLoaders: 2,
|
||||
},
|
||||
sassLoaderOptions: {
|
||||
sassOptions: {
|
||||
// silencing these warnings since we're planning to remove sass when angular is gone
|
||||
silenceDeprecations: ['import', 'global-builtin'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@storybook/addon-webpack5-compiler-swc',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['static'],
|
||||
webpackFinal: async (config) => {
|
||||
const webpack = await import('webpack');
|
||||
|
||||
// Define process.env for browser context
|
||||
config.plugins?.push(
|
||||
new webpack.default.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
'process.env.STORYBOOK_THEME': JSON.stringify(process.env.STORYBOOK_THEME || 'system'),
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Preview } from '@storybook/react';
|
||||
|
||||
import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { withTheme } from '../src/utils/storybook/withTheme';
|
||||
import { withTimeZone } from '../src/utils/storybook/withTimeZone';
|
||||
|
||||
const allowedExtraThemes: string[] = [];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
allowedExtraThemes.push('debug');
|
||||
allowedExtraThemes.push('desertbloom');
|
||||
allowedExtraThemes.push('gildedgrove');
|
||||
allowedExtraThemes.push('gloom');
|
||||
allowedExtraThemes.push('sapphiredusk');
|
||||
allowedExtraThemes.push('tron');
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [withTheme(), withTimeZone()],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'system',
|
||||
toolbar: {
|
||||
icon: 'paintbrush',
|
||||
items: getBuiltInThemes(allowedExtraThemes).map((theme) => ({
|
||||
value: theme.id,
|
||||
title: theme.name,
|
||||
})),
|
||||
showName: true,
|
||||
},
|
||||
},
|
||||
timeZone: {
|
||||
description: 'Set the timezone for the storybook preview',
|
||||
defaultValue: getTimeZone(),
|
||||
toolbar: {
|
||||
icon: 'globe',
|
||||
items: getTimeZones(true)
|
||||
.filter((timezone) => !!timezone)
|
||||
.map((timezone) => ({
|
||||
title: timezone,
|
||||
value: timezone,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: process.env.STORYBOOK_THEME || 'system',
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -36,7 +36,8 @@
|
||||
"clean": "rimraf ./dist ./compiled ./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",
|
||||
"storybook": "storybook dev -p 6006"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
@@ -60,11 +61,15 @@
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@storybook/addon-webpack5-compiler-swc": "^4.0.2",
|
||||
"@storybook/react": "^10.0.8",
|
||||
"@storybook/react-webpack5": "^10.0.8",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.1.2",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/d3": "^7",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "24.10.1",
|
||||
@@ -73,11 +78,13 @@
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"babel-jest": "29.7.0",
|
||||
"esbuild": "0.25.8",
|
||||
"fs-extra": "^11.3.2",
|
||||
"jest": "^29.6.4",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"storybook": "^10.0.8",
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { ColorScheme, SelectedView } from '../types';
|
||||
|
||||
import FlameGraph from './FlameGraph';
|
||||
import { CollapsedMap, FlameGraphDataContainer } from './dataTransform';
|
||||
import { data } from './testData/dataBasic';
|
||||
|
||||
const meta: Meta<typeof FlameGraph> = {
|
||||
title: 'FlameGraph',
|
||||
component: FlameGraph,
|
||||
args: {
|
||||
rangeMin: 0,
|
||||
rangeMax: 1,
|
||||
textAlign: 'left',
|
||||
colorScheme: ColorScheme.PackageBased,
|
||||
selectedView: SelectedView.Both,
|
||||
search: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {
|
||||
render: (args) => {
|
||||
const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: false });
|
||||
|
||||
return <FlameGraph {...args} data={dataContainer} collapsedMap={new CollapsedMap()} />;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DataFrameDTO } from '@grafana/data';
|
||||
|
||||
export const data: DataFrameDTO = {
|
||||
name: 'response',
|
||||
refId: 'A',
|
||||
// @ts-ignore
|
||||
meta: { preferredVisualisationType: 'flamegraph' },
|
||||
fields: [
|
||||
{
|
||||
name: 'level',
|
||||
values: [0, 1, 2, 3, 2, 1],
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
values: [10000, 5000, 4000, 3000, 500, 3000],
|
||||
config: {
|
||||
unit: 'ms',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'self',
|
||||
values: [10000, 500, 1000, 3000, 500, 3000],
|
||||
config: {
|
||||
unit: 'ms',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
values: ['total', 'fn_1', 'fn_2', 'fn_3', 'fn_4', 'fn_5'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { data } from './FlameGraph/testData/dataNestedSet';
|
||||
import FlameGraphContainer, { Props } from './FlameGraphContainer';
|
||||
|
||||
const WrappedFlameGraph = (props: Omit<Props, 'getTheme'>) => {
|
||||
const theme = useTheme2();
|
||||
const df = createDataFrame(data);
|
||||
return <FlameGraphContainer {...props} data={df} getTheme={() => theme} />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof FlameGraphContainer> = {
|
||||
title: 'FlameGraphContainer',
|
||||
render: (args) => {
|
||||
return <WrappedFlameGraph {...args} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||
import { ColorScheme } from '../types';
|
||||
|
||||
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
|
||||
|
||||
const meta: Meta<typeof FlameGraphTopTableContainer> = {
|
||||
title: 'TopTable',
|
||||
component: FlameGraphTopTableContainer,
|
||||
args: {
|
||||
colorScheme: ColorScheme.ValueBased,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: '100%', height: '600px' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {
|
||||
render: (args) => {
|
||||
const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: true });
|
||||
|
||||
return (
|
||||
<FlameGraphTopTableContainer
|
||||
{...args}
|
||||
data={dataContainer}
|
||||
onSymbolClick={() => {}}
|
||||
onSearch={() => {}}
|
||||
onSandwich={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -144,6 +144,7 @@ function buildFilteredTable(data: FlameGraphDataContainer, matchedLabels?: Set<s
|
||||
// Add current call to the stack
|
||||
callStack.push(label);
|
||||
}
|
||||
|
||||
return filteredTable;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getThemeById, ThemeContext } from '@grafana/data';
|
||||
import { GlobalStyles } from '@grafana/ui';
|
||||
|
||||
interface ThemeableStoryProps {
|
||||
themeId: string;
|
||||
}
|
||||
const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<ThemeableStoryProps>) => {
|
||||
const theme = getThemeById(themeId);
|
||||
|
||||
const css = `
|
||||
#storybook-root {
|
||||
padding: ${theme.spacing(2)};
|
||||
}
|
||||
|
||||
body {
|
||||
background: ${theme.colors.background.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<GlobalStyles />
|
||||
|
||||
<style>{css}</style>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const withTheme =
|
||||
(): Decorator =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
(story, context) => <ThemeableStory themeId={context.globals.theme}>{story()}</ThemeableStory>;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { setTimeZoneResolver } from '@grafana/data';
|
||||
|
||||
export const withTimeZone = (): Decorator => (Story, context) => {
|
||||
useEffect(() => {
|
||||
setTimeZoneResolver(() => context.globals.timeZone ?? 'browser');
|
||||
}, [context.globals.timeZone]);
|
||||
|
||||
return Story();
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
UnifiedAlertingConfig,
|
||||
GrafanaConfig,
|
||||
CurrentUserDTO,
|
||||
PluginMetasSpec,
|
||||
} from '@grafana/data';
|
||||
|
||||
/**
|
||||
@@ -266,6 +267,11 @@ export class GrafanaBootConfig {
|
||||
listScopesEndpoint = '';
|
||||
|
||||
openFeatureContext: Record<string, unknown> = {};
|
||||
plugins: Record<'apps' | 'panels' | 'datasources', Record<string, PluginMetasSpec>> = {
|
||||
apps: {},
|
||||
datasources: {},
|
||||
panels: {},
|
||||
};
|
||||
|
||||
constructor(
|
||||
options: BootData['settings'] & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { dateTime, makeTimeRange, TimeRange } from '@grafana/data';
|
||||
import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { TimeRangeProvider } from './TimeRangeContext';
|
||||
@@ -152,6 +152,58 @@ it('does not submit wrapping forms', async () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows CTRL+Z in zoom out tooltip when feature flag is disabled', async () => {
|
||||
window.grafanaBootData = {
|
||||
settings: {
|
||||
featureToggles: {
|
||||
newTimeRangeZoomShortcuts: false,
|
||||
},
|
||||
},
|
||||
} as BootData;
|
||||
|
||||
render(
|
||||
<TimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
onChange={(value) => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const zoomButton = screen.getByLabelText('Zoom out time range');
|
||||
await userEvent.hover(zoomButton);
|
||||
|
||||
expect(await screen.findByText(/CTRL\+Z/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows t - in zoom out tooltip when feature flag is enabled', async () => {
|
||||
window.grafanaBootData = {
|
||||
settings: {
|
||||
featureToggles: {
|
||||
newTimeRangeZoomShortcuts: true,
|
||||
},
|
||||
},
|
||||
} as BootData;
|
||||
|
||||
render(
|
||||
<TimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
onChange={(value) => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const zoomButton = screen.getByLabelText('Zoom out time range');
|
||||
await userEvent.hover(zoomButton);
|
||||
|
||||
expect(await screen.findByText(/t -/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('TimePickerTooltip', () => {
|
||||
beforeAll(() => {
|
||||
const mockIntl = {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFeatureToggle } from '../../utils/featureToggle';
|
||||
import { ButtonGroup } from '../Button/ButtonGroup';
|
||||
import { getModalStyles } from '../Modal/getModalStyles';
|
||||
import { getPortalContainer } from '../Portal/Portal';
|
||||
@@ -243,13 +244,22 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
|
||||
TimeRangePicker.displayName = 'TimeRangePicker';
|
||||
|
||||
const ZoomOutTooltip = () => (
|
||||
<>
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
const ZoomOutTooltip = () => {
|
||||
const newShortcuts = getFeatureToggle('newTimeRangeZoomShortcuts');
|
||||
return (
|
||||
<>
|
||||
{newShortcuts ? (
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip-new">
|
||||
Time range zoom out <br /> t -
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</Trans>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {
|
||||
const styles = useStyles2(getLabelStyles);
|
||||
|
||||
@@ -59,8 +59,9 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) {
|
||||
{context.hasOpenPane && (
|
||||
<SidebarButton
|
||||
icon={'web-section-alt'}
|
||||
onClick={context.onDockChange}
|
||||
onClick={context.onToggleDock}
|
||||
title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')}
|
||||
data-testid="sidebar-dock-toggle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useContext } from 'react';
|
||||
import React, { ButtonHTMLAttributes, useContext } from 'react';
|
||||
|
||||
import { GrafanaTheme2, IconName, isIconName } from '@grafana/data';
|
||||
|
||||
@@ -11,38 +11,48 @@ import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
import { SidebarContext } from './useSidebar';
|
||||
|
||||
export interface Props {
|
||||
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon: IconName;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ icon, active, onClick, title, tooltip, ...restProps }, ref) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Sidebar.Button must be used within a Sidebar component');
|
||||
if (!context) {
|
||||
throw new Error('Sidebar.Button must be used within a Sidebar component');
|
||||
}
|
||||
|
||||
const buttonClass = cx(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip ref={ref} content={tooltip ?? title} placement={context.position === 'left' ? 'right' : 'left'}>
|
||||
<button
|
||||
className={buttonClass}
|
||||
aria-label={title}
|
||||
aria-expanded={active}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const buttonClass = cx(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip ?? title} placement={context.position === 'left' ? 'right' : 'left'}>
|
||||
<button className={buttonClass} aria-label={title} aria-expanded={active} type="button" onClick={onClick}>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
SidebarButton.displayName = 'SidebarButton';
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) {
|
||||
if (!icon) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
@@ -27,6 +28,7 @@ export function SidebarPaneHeader({ children, onClose, title }: Props) {
|
||||
onClick={onClose}
|
||||
aria-label={t('grafana-ui.sidebar.close', 'Close')}
|
||||
tooltip={t('grafana-ui.sidebar.close', 'Close')}
|
||||
data-testid={selectors.components.Sidebar.closePane}
|
||||
/>
|
||||
)}
|
||||
<Text weight="medium" variant="h6" truncate data-testid="sidebar-pane-header-title">
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface SidebarContextValue {
|
||||
bottomMargin: number;
|
||||
edgeMargin: number;
|
||||
contentMargin: number;
|
||||
onDockChange: () => void;
|
||||
onToggleDock: () => void;
|
||||
onResize: (diff: number) => void;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function useSidebar({
|
||||
// Used to accumulate drag distance to know when to change compact mode
|
||||
const [_, setCompactDrag] = React.useState(0);
|
||||
|
||||
const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
|
||||
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
||||
const toolbarWidth =
|
||||
@@ -98,7 +98,7 @@ export function useSidebar({
|
||||
|
||||
return {
|
||||
isDocked,
|
||||
onDockChange,
|
||||
onToggleDock,
|
||||
onResize,
|
||||
outerWrapperProps,
|
||||
position,
|
||||
|
||||
@@ -29,7 +29,7 @@ const cursorDefaults: Cursor = {
|
||||
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number };
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean };
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
readonly uid = Math.random().toString(36).slice(2);
|
||||
|
||||
+15
-1
@@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
expect(mockQueryZoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isPanning state during drag and clear on mouseup', () => {
|
||||
it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
@@ -153,6 +153,20 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({
|
||||
isPanning: true,
|
||||
min: expectedRange.from,
|
||||
max: expectedRange.to,
|
||||
isTimeRangePending: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear isPanning state immediately for small drags below threshold', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,11 +96,14 @@ export const setupXAxisPan = (
|
||||
|
||||
xAxisEl.style.cursor = 'grab';
|
||||
|
||||
config.setState({ isPanning: false });
|
||||
const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST;
|
||||
|
||||
if (Math.abs(dragPixels) >= MIN_PAN_DIST) {
|
||||
if (isSignificantDrag) {
|
||||
const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width);
|
||||
config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true });
|
||||
queryZoom(newRange);
|
||||
} else {
|
||||
config.setState({ isPanning: false });
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/emicklei/go-restful/v3 v3.13.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
|
||||
|
||||
@@ -101,8 +101,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/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e h1:vheR6iPO1np+G/ARjcWx9yiWd7BnTDgyTgsnMhOvx70=
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -200,6 +202,11 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
|
||||
|
||||
if err != nil {
|
||||
@@ -231,6 +238,11 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
|
||||
if uid == "" {
|
||||
@@ -361,6 +373,11 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
cmd := datasources.AddDataSourceCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
@@ -478,6 +495,10 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
cmd := datasources.UpdateDataSourceCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
@@ -81,6 +83,19 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
|
||||
}, mockSQLStore)
|
||||
}
|
||||
|
||||
// setupDsConfigMetrics creates and registers the prometheus metrics needed for HTTPServer tests
|
||||
// that call methods using dsConfigHandlerRequestsDuration.
|
||||
func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) {
|
||||
promRegister := prometheus.NewRegistry()
|
||||
dsConfigHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "ds_config_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by datasource configuration handlers",
|
||||
}, []string{"code_path", "handler"})
|
||||
promRegister.MustRegister(dsConfigHandlerRequestsDuration)
|
||||
return promRegister, dsConfigHandlerRequestsDuration
|
||||
}
|
||||
|
||||
// Adding data sources with invalid URLs should lead to an error.
|
||||
func TestAddDataSource_InvalidURL(t *testing.T) {
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
@@ -88,6 +103,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
|
||||
@@ -118,6 +134,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
|
||||
@@ -143,6 +160,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
|
||||
@@ -175,6 +193,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -199,6 +218,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
@@ -236,6 +256,7 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) {
|
||||
ExpectedErr: nil,
|
||||
},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
|
||||
@@ -289,6 +310,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
@@ -429,6 +451,7 @@ func TestAPI_datasources_AccessControl(t *testing.T) {
|
||||
hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}}
|
||||
hs.accesscontrolService = actest.FakeService{}
|
||||
hs.Live = newTestLive(t, hs.SQLStore)
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
})
|
||||
|
||||
for _, url := range tt.urls {
|
||||
|
||||
+28
-21
@@ -203,27 +203,28 @@ type HTTPServer struct {
|
||||
pluginsCDNService *pluginscdn.Service
|
||||
managedPluginsService managedplugins.Manager
|
||||
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
orgDeletionService org.DeletionService
|
||||
TeamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
promRegister prometheus.Registerer
|
||||
promGatherer prometheus.Gatherer
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
namespacer request.NamespaceMapper
|
||||
anonService anonymous.Service
|
||||
userVerifier user.Verifier
|
||||
tlsCerts TLSCerts
|
||||
htmlHandlerRequestsDuration *prometheus.HistogramVec
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
orgDeletionService org.DeletionService
|
||||
TeamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
promRegister prometheus.Registerer
|
||||
promGatherer prometheus.Gatherer
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
namespacer request.NamespaceMapper
|
||||
anonService anonymous.Service
|
||||
userVerifier user.Verifier
|
||||
tlsCerts TLSCerts
|
||||
htmlHandlerRequestsDuration *prometheus.HistogramVec
|
||||
dsConfigHandlerRequestsDuration *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
type TLSCerts struct {
|
||||
@@ -382,9 +383,15 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
Name: "html_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by the index.go HTML handler",
|
||||
}, []string{"handler"}),
|
||||
dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "ds_config_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by datasource configuration handlers",
|
||||
}, []string{"code_path", "handler"}),
|
||||
}
|
||||
|
||||
promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
|
||||
promRegister.MustRegister(hs.dsConfigHandlerRequestsDuration)
|
||||
|
||||
if hs.Listener != nil {
|
||||
hs.log.Debug("Using provided listener")
|
||||
|
||||
@@ -187,7 +187,7 @@ func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{},
|
||||
case data.FieldTypeBool, data.FieldTypeNullableBool:
|
||||
return parseBoolFromInt8(val, nullable)
|
||||
|
||||
case data.FieldTypeJSON, data.FieldTypeNullableJSON:
|
||||
case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck
|
||||
switch v := val.(type) {
|
||||
case types.JSONDocument:
|
||||
raw := json.RawMessage(v.String())
|
||||
|
||||
@@ -182,7 +182,7 @@ func convertDataType(fieldType data.FieldType) mysql.Type {
|
||||
return types.Boolean
|
||||
case data.FieldTypeTime, data.FieldTypeNullableTime:
|
||||
return types.Timestamp
|
||||
case data.FieldTypeJSON, data.FieldTypeNullableJSON:
|
||||
case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck
|
||||
return types.JSON
|
||||
default:
|
||||
fmt.Printf("------- Unsupported field type: %v", fieldType)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -21,6 +22,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
var openfeatureClient = openfeature.NewDefaultClient()
|
||||
|
||||
const (
|
||||
pluginPageFeatureFlagPrefix = "plugin-page-visible."
|
||||
)
|
||||
|
||||
type AuthOptions struct {
|
||||
@@ -146,6 +154,12 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, log
|
||||
return
|
||||
}
|
||||
|
||||
if !PageIsFeatureToggleEnabled(c.Req.Context(), c.Req.URL.Path) {
|
||||
logger.Debug("Forbidden experimental plugin page", "plugin", pluginID, "path", c.Req.URL.Path)
|
||||
accessForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
permitted := true
|
||||
path := normalizeIncludePath(c.Req.URL.Path)
|
||||
hasAccess := ac.HasAccess(accessControl, c)
|
||||
@@ -294,3 +308,18 @@ func shouldForceLogin(c *contextmodel.ReqContext) bool {
|
||||
|
||||
return forceLogin
|
||||
}
|
||||
|
||||
// PageIsFeatureToggleEnabled checks if a page is enabled via OpenFeature feature flags.
|
||||
// It returns false if the feature flag is set and set to false.
|
||||
// The feature flag key format is: "plugin-page-visible.<path>"
|
||||
func PageIsFeatureToggleEnabled(ctx context.Context, path string) bool {
|
||||
flagKey := pluginPageFeatureFlagPrefix + filepath.Clean(path)
|
||||
enabled := openfeatureClient.Boolean(
|
||||
ctx,
|
||||
flagKey,
|
||||
true,
|
||||
openfeature.TransactionContext(ctx),
|
||||
)
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
oftesting "github.com/open-feature/go-sdk/openfeature/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -28,6 +33,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
var openfeatureTestMutex sync.Mutex
|
||||
|
||||
func setupAuthMiddlewareTest(t *testing.T, identity *authn.Identity, authErr error) *contexthandler.ContextHandler {
|
||||
return contexthandler.ProvideService(setting.NewCfg(), &authntest.FakeService{
|
||||
ExpectedErr: authErr,
|
||||
@@ -422,6 +429,60 @@ func TestCanAdminPlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageIsFeatureToggleEnabled(t *testing.T) {
|
||||
type testCase struct {
|
||||
desc string
|
||||
path string
|
||||
flags map[string]bool
|
||||
expectedResult bool
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
desc: "returns true when feature flag is enabled",
|
||||
path: "/a/my-plugin/settings",
|
||||
flags: map[string]bool{
|
||||
pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": true,
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
desc: "returns false when feature flag is disabled",
|
||||
path: "/a/my-plugin/settings",
|
||||
flags: map[string]bool{
|
||||
pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false,
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
desc: "returns false when feature flag is disabled with trailing slash",
|
||||
path: "/a/my-plugin/settings/",
|
||||
flags: map[string]bool{
|
||||
pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false,
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
desc: "returns true when feature flag does not exist",
|
||||
path: "/a/my-plugin/settings",
|
||||
flags: map[string]bool{},
|
||||
expectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
setupTestProvider(t, tt.flags)
|
||||
|
||||
result := PageIsFeatureToggleEnabled(ctx, tt.path)
|
||||
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler {
|
||||
return func(c *web.Context) {
|
||||
reqCtx := &contextmodel.ReqContext{
|
||||
@@ -437,3 +498,38 @@ func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler
|
||||
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx))
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestProvider creates a test OpenFeature provider with the given flags.
|
||||
// Uses a global lock to prevent concurrent provider changes across tests.
|
||||
func setupTestProvider(t *testing.T, flags map[string]bool) oftesting.TestProvider {
|
||||
t.Helper()
|
||||
|
||||
// Lock to prevent concurrent provider changes
|
||||
openfeatureTestMutex.Lock()
|
||||
|
||||
testProvider := oftesting.NewTestProvider()
|
||||
flagsMap := map[string]memprovider.InMemoryFlag{}
|
||||
|
||||
for key, value := range flags {
|
||||
flagsMap[key] = memprovider.InMemoryFlag{
|
||||
DefaultVariant: "defaultVariant",
|
||||
Variants: map[string]any{
|
||||
"defaultVariant": value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testProvider.UsingFlags(t, flagsMap)
|
||||
|
||||
err := openfeature.SetProviderAndWait(testProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
testProvider.Cleanup()
|
||||
_ = openfeature.SetProviderAndWait(openfeature.NoopProvider{})
|
||||
// Unlock after cleanup to allow other tests to run
|
||||
openfeatureTestMutex.Unlock()
|
||||
})
|
||||
|
||||
return testProvider
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user