Merge remote-tracking branch 'origin/main' into ds-apiserver-schema-builder
This commit is contained in:
@@ -16,7 +16,7 @@ const grafanaI18nPlugin = require('@grafana/i18n/eslint-plugin');
|
||||
// as we just want to pull in all of the necessary configuration but not run the rules
|
||||
// (this should only be concerned with checking rules that we want to improve,
|
||||
// so there's no need to try and run the rules that will be linted properly anyway)
|
||||
const mappedBaseConfigs = grafanaConfig.map((config) => {
|
||||
const mappedBaseConfigs = grafanaConfig.map((/** @type {import('eslint').Linter.Config} */ config) => {
|
||||
const { rules, ...baseConfig } = config;
|
||||
return baseConfig;
|
||||
});
|
||||
|
||||
+31
-22
@@ -1534,6 +1534,13 @@ exports[`better eslint`] = {
|
||||
"public/app/features/alerting/unified/rule-list/StateView.tsx:5381": [
|
||||
[0, 0, 0, "Using localeCompare() can cause performance issues when sorting large datasets. Consider using Intl.Collator for better performance when sorting arrays, or add an eslint-disable comment if sorting a small, known dataset.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/types/alerting.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/features/alerting/unified/types/receiver-form.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -1702,15 +1709,21 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "React Hook \\"useSnappingSplitter\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
@@ -1884,18 +1897,26 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "9"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"],
|
||||
[0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "6"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "7"],
|
||||
[0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "8"],
|
||||
[0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "9"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
@@ -3974,9 +3995,6 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/table/cells/TextWrapOptionsEditor.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/table/migrations.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
@@ -4053,13 +4071,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/types/alerting.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/types/dashboard.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -4353,8 +4364,6 @@ exports[`no gf-form usage`] = {
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
|
||||
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx:5381": [
|
||||
|
||||
@@ -969,7 +969,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
|
||||
/public/app/routes/ @grafana/grafana-search-navigate-organise
|
||||
/public/app/store/ @grafana/grafana-frontend-platform
|
||||
/public/app/types/ @grafana/grafana-frontend-platform
|
||||
/public/app/types/alerting.ts @grafana/alerting-frontend
|
||||
/public/app/types/unified-alerting-dto.ts @grafana/alerting-frontend
|
||||
/public/app/types/unified-alerting.ts @grafana/alerting-frontend
|
||||
/public/dashboards/ @grafana/dashboards-squad
|
||||
|
||||
@@ -102,6 +102,7 @@ COPY apps/investigations apps/investigations
|
||||
COPY apps/advisor apps/advisor
|
||||
COPY apps/dashboard apps/dashboard
|
||||
COPY apps/folder apps/folder
|
||||
COPY apps/preferences apps/preferences
|
||||
COPY apps/iam apps/iam
|
||||
COPY apps apps
|
||||
COPY kindsv2 kindsv2
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
|
||||
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
|
||||
@@ -8,6 +8,7 @@ ReceiverSpec: {
|
||||
#Integration: {
|
||||
uid?: string
|
||||
type: string
|
||||
version: string
|
||||
disableResolveMessage?: bool
|
||||
settings: {
|
||||
[string]: _
|
||||
|
||||
@@ -6,6 +6,7 @@ package v0alpha1
|
||||
type ReceiverIntegration struct {
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
SecureFields map[string]bool `json:"secureFields,omitempty"`
|
||||
|
||||
@@ -108,6 +108,13 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"version": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"disableResolveMessage": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"boolean"},
|
||||
@@ -144,7 +151,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"type", "settings"},
|
||||
Required: []string{"type", "version", "settings"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ import (
|
||||
// {
|
||||
// "matcher": { "id": "byName", "options": "Hidden" },
|
||||
// "properties": [
|
||||
// { "id": "custom.hidden", "value": true }
|
||||
// { "id": "custom.hideFrom.viz", "value": true }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
@@ -436,7 +436,7 @@ func migrateTableStyleToOverride(style map[string]interface{}) map[string]interf
|
||||
// Handle hidden type
|
||||
if styleType, ok := style["type"].(string); ok && styleType == "hidden" {
|
||||
properties = append(properties, map[string]interface{}{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ func TestV24(t *testing.T) {
|
||||
"options": "Hidden",
|
||||
},
|
||||
"properties": []interface{}{
|
||||
map[string]interface{}{"id": "custom.hidden", "value": true},
|
||||
map[string]interface{}{"id": "custom.hideFrom.viz", "value": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Local development overrides
|
||||
local/yamls/iam-secrets.yaml
|
||||
+2
-28
@@ -1,7 +1,5 @@
|
||||
include ../sdk.mk
|
||||
|
||||
OPERATOR_DOCKERIMAGE := "github.com/grafana/grafana/apps/iam/operator"
|
||||
|
||||
.PHONY: generate
|
||||
generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation
|
||||
@$(APP_SDK_BIN) generate \
|
||||
@@ -17,17 +15,9 @@ deps:
|
||||
@go mod tidy
|
||||
@GOWORK=off go mod vendor
|
||||
|
||||
.PHONY: build
|
||||
build: build/operator
|
||||
|
||||
.PHONY: build/operator
|
||||
build/operator:
|
||||
docker build -t $(OPERATOR_DOCKERIMAGE) -f cmd/operator/Dockerfile .
|
||||
|
||||
.PHONY: local/up
|
||||
local/up:
|
||||
@./local/scripts/cluster.sh create "local/k3d-config.json"
|
||||
@cd local && tilt up
|
||||
@tilt up
|
||||
|
||||
.PHONY: local/generate
|
||||
local/generate:
|
||||
@@ -35,20 +25,4 @@ local/generate:
|
||||
|
||||
.PHONY: local/down
|
||||
local/down:
|
||||
@cd local && tilt down
|
||||
|
||||
.PHONY: local/deploy_plugin
|
||||
local/deploy_plugin:
|
||||
-tilt disable grafana
|
||||
cp -R plugin/dist local/mounted-files/plugin/dist
|
||||
-tilt enable grafana
|
||||
|
||||
.PHONY: local/push_operator
|
||||
local/push_operator: build/operator
|
||||
# Tag the docker image as part of localhost, which is what the generated k8s uses to avoid confusion with the real operator image
|
||||
@docker tag "$(OPERATOR_DOCKERIMAGE):latest" "localhost/$(OPERATOR_DOCKERIMAGE):latest"
|
||||
@./local/scripts/push_image.sh "localhost/$(OPERATOR_DOCKERIMAGE):latest"
|
||||
|
||||
.PHONY: local/clean
|
||||
local/clean: local/down
|
||||
@./local/scripts/cluster.sh delete
|
||||
@tilt down
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
# version_settings() enforces a minimum Tilt version
|
||||
# https://docs.tilt.dev/api.html#api.version_settings
|
||||
version_settings(constraint='>=0.22.2')
|
||||
|
||||
custom_build(
|
||||
'grafana-iam-operator',
|
||||
command='docker build -t $EXPECTED_REF -f cmd/operator/Dockerfile .',
|
||||
deps=[
|
||||
'cmd/operator',
|
||||
'pkg',
|
||||
],
|
||||
disable_push=True,
|
||||
)
|
||||
|
||||
k8s_yaml([filename for filename in listdir('local/yamls') if filename.lower().endswith(('.yaml', '.yml'))])
|
||||
@@ -89,11 +89,11 @@ func LoadConfigFromEnv() (*Config, error) {
|
||||
}
|
||||
cfg.KubeConfig = kubeConfig
|
||||
} else if folderAppURL := os.Getenv("FOLDER_APP_URL"); folderAppURL != "" {
|
||||
exchangeUrl := os.Getenv("AUTH_TOKEN_EXCHANGE_URL")
|
||||
exchangeUrl := os.Getenv("TOKEN_EXCHANGE_URL")
|
||||
authToken := os.Getenv("AUTH_TOKEN")
|
||||
namespace := os.Getenv("FOLDER_APP_NAMESPACE")
|
||||
if exchangeUrl == "" || authToken == "" {
|
||||
return nil, fmt.Errorf("AUTH_TOKEN_EXCHANGE_URL and AUTH_TOKEN must be set when FOLDER_APP_URL is set")
|
||||
return nil, fmt.Errorf("TOKEN_EXCHANGE_URL and AUTH_TOKEN must be set when FOLDER_APP_URL is set")
|
||||
}
|
||||
|
||||
kubeConfig, err := LoadKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace)
|
||||
@@ -111,7 +111,7 @@ func LoadConfigFromEnv() (*Config, error) {
|
||||
|
||||
cfg.ZanzanaClient.Address = os.Getenv("ZANZANA_ADDR")
|
||||
cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN")
|
||||
cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("ZANZANA_TOKEN_EXCHANGE_URL")
|
||||
cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL")
|
||||
cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE")
|
||||
|
||||
cfg.FolderReconciler.Namespace = os.Getenv("FOLDER_RECONCILER_NAMESPACE")
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ require (
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/storage v1.55.0 // indirect
|
||||
cuelang.org/go v0.11.1 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||
|
||||
+2
-2
@@ -80,8 +80,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSB
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
|
||||
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
|
||||
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
|
||||
+30
-2
@@ -8,13 +8,41 @@ userKind: {
|
||||
kind: "User"
|
||||
pluralName: "Users"
|
||||
codegen: {
|
||||
ts: { enabled: false }
|
||||
go: { enabled: true }
|
||||
ts: {enabled: false}
|
||||
go: {enabled: true}
|
||||
}
|
||||
}
|
||||
|
||||
userv0alpha1: userKind & {
|
||||
// TODO: Uncomment this when User will be added to ManagedKinds
|
||||
// validation: {
|
||||
// operations: [
|
||||
// "CREATE",
|
||||
// "UPDATE",
|
||||
// ]
|
||||
// }
|
||||
// mutation: {
|
||||
// operations: [
|
||||
// "CREATE",
|
||||
// "UPDATE",
|
||||
// ]
|
||||
// }
|
||||
schema: {
|
||||
spec: v0alpha1.UserSpec
|
||||
}
|
||||
// TODO: Uncomment when the custom routes implementation is done
|
||||
// routes: {
|
||||
// "/teams": {
|
||||
// "GET": {
|
||||
// response: {
|
||||
// #UserTeam: {
|
||||
// title: string
|
||||
// teamRef: v0alpha1.TeamRef
|
||||
// permission: v0alpha1.TeamPermission
|
||||
// }
|
||||
// items: [...#UserTeam]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ ResourcePermission: {
|
||||
kind: "User" | "ServiceAccount" | "Team" | "BasicRole"
|
||||
// uid of the identity getting the permission
|
||||
name: string
|
||||
// list of actions granted to the user (e.g. "admin" or "get", "update")
|
||||
verbs: [...string]
|
||||
// action set granted to the user (e.g. "admin" or "edit", "view")
|
||||
verb: string
|
||||
}
|
||||
|
||||
resource: #Resource
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
# version_settings() enforces a minimum Tilt version
|
||||
# https://docs.tilt.dev/api.html#api.version_settings
|
||||
version_settings(constraint='>=0.22.2')
|
||||
|
||||
def name(c):
|
||||
return c['metadata']['name']
|
||||
|
||||
def namespace(c):
|
||||
if 'namespace' in c['metadata']:
|
||||
return c['metadata']['namespace']
|
||||
return ''
|
||||
|
||||
def decode(yaml):
|
||||
resources = decode_yaml_stream(yaml)
|
||||
|
||||
# workaround a bug in decode_yaml_stream where it returns duplicates
|
||||
# This bug has been fixed in Tilt v0.17.3+
|
||||
filtered = []
|
||||
names = {}
|
||||
for r in resources:
|
||||
if r == None:
|
||||
continue
|
||||
|
||||
n = '%s:%s:%s' % (name(r), r['kind'], namespace(r))
|
||||
if n in names:
|
||||
continue
|
||||
|
||||
names[n] = True
|
||||
filtered.append(r)
|
||||
|
||||
return filtered
|
||||
|
||||
def find_overlapping(o, yamls):
|
||||
for elem in yamls:
|
||||
if name(o) == name(elem) and o['kind'] == elem['kind'] and namespace(o) == namespace(elem):
|
||||
return elem
|
||||
return None
|
||||
|
||||
yaml_objects = []
|
||||
# Parse all YAML files in our "yamls" directory
|
||||
for filename in listdir('yamls'):
|
||||
if filename.lower().endswith(('.yaml', '.yml')):
|
||||
decoded = decode(read_file(filename))
|
||||
for o in decoded:
|
||||
present = find_overlapping(o, yaml_objects)
|
||||
if present != None:
|
||||
print("Overlapping resource found: %s", filename)
|
||||
exit(1)
|
||||
yaml_objects += decoded
|
||||
|
||||
bundle = encode_yaml_stream(yaml_objects)
|
||||
|
||||
# k8s_yaml automatically creates resources in Tilt for the entities
|
||||
# and will inject any images referenced in the Tiltfile when deploying
|
||||
# https://docs.tilt.dev/api.html#api.k8s_yaml
|
||||
k8s_yaml(bundle)
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"apiVersion": "k3d.io/v1alpha3",
|
||||
"kind": "Simple",
|
||||
"kubeAPI": {
|
||||
"hostPort": "8556"
|
||||
},
|
||||
"options": {
|
||||
"k3d": {
|
||||
"wait": true
|
||||
},
|
||||
"kubeconfig": {
|
||||
"switchCurrentContext": true,
|
||||
"updateDefaultKubeconfig": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eufo pipefail
|
||||
|
||||
CLUSTER_NAME="grafana-iam-operator"
|
||||
|
||||
create_cluster() {
|
||||
K3D_CONFIG="${1:-k3d-config.json}"
|
||||
|
||||
if ! k3d cluster list "${CLUSTER_NAME}" >/dev/null 2>&1; then
|
||||
# Array of extra options to add to the k3d cluster create command
|
||||
EXTRA_K3D_OPTS=()
|
||||
|
||||
# Bug in k3d for btrfs filesystems workaround, see https://k3d.io/v5.2.2/faq/faq/#issues-with-btrfs
|
||||
# Apple is APFS/HFS and stat has a different API, so we might as well skip that
|
||||
if [[ "${OSTYPE}" != "darwin*" ]]; then
|
||||
ROOTFS="$(stat -f --format="%T" "/")"
|
||||
if [[ "${ROOTFS}" == "btrfs" ]]; then
|
||||
EXTRA_K3D_OPTS+=("-v" "/dev/mapper:/dev/mapper")
|
||||
fi
|
||||
fi
|
||||
|
||||
k3d cluster create "${CLUSTER_NAME}" --config "${K3D_CONFIG}" ${EXTRA_K3D_OPTS[@]+"${EXTRA_K3D_OPTS[@]}"}
|
||||
else
|
||||
echo "Cluster already exists"
|
||||
fi
|
||||
}
|
||||
|
||||
delete_cluster() {
|
||||
k3d cluster delete "${CLUSTER_NAME}"
|
||||
}
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: ./cluster.sh [create|delete]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $1 == "create" ]; then
|
||||
create_cluster $2
|
||||
elif [ $1 == "delete" ]; then
|
||||
delete_cluster
|
||||
else
|
||||
echo "Unknown argument ${1}"
|
||||
fi
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eufo pipefail
|
||||
|
||||
CLUSTER_NAME="grafana-iam-operator"
|
||||
IMAGE="$1"
|
||||
|
||||
if [[ "$IMAGE" == "" ]]; then
|
||||
echo "usage: push_image.sh <image_name:tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
k3d image import "${IMAGE}" -c "${CLUSTER_NAME}"
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
spec:
|
||||
serviceAccount: operator
|
||||
containers:
|
||||
- image: localhost/github.com/grafana/grafana/apps/iam/operator:latest
|
||||
- image: grafana-iam-operator
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: iam-app-operator
|
||||
command: ["/bin/sh"]
|
||||
@@ -31,15 +31,28 @@ spec:
|
||||
- -c
|
||||
- exec /usr/bin/operator
|
||||
env:
|
||||
- name: KUBE_FEATURE_WatchListClient
|
||||
value: "false"
|
||||
- name: ZANZANA_ADDR
|
||||
value: zanzana.default.svc.cluster.local:50051
|
||||
- name: FOLDER_APP_URL
|
||||
value: https://host.docker.internal:6446
|
||||
- name: AUTH_TOKEN_EXCHANGE_URL
|
||||
- name: FOLDER_APP_NAMESPACE
|
||||
value: grafana-folder
|
||||
- name: TOKEN_EXCHANGE_URL
|
||||
value: http://host.docker.internal:8080/v1/sign-access-token
|
||||
- name: AUTH_TOKEN
|
||||
value: ""
|
||||
- name: FOLDER_APP_NAMESPACE
|
||||
value: "grafana-folder"
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: iam-app-operator-secrets
|
||||
key: auth-token
|
||||
- name: ZANZANA_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: iam-app-operator-secrets
|
||||
key: zanzana-token
|
||||
- name: FOLDER_RECONCILER_NAMESPACE
|
||||
value: "default"
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: iam-app-operator-secrets
|
||||
key: folder-reconciler-namespace
|
||||
|
||||
@@ -88,12 +88,8 @@ metadata:
|
||||
name: zanzana
|
||||
data:
|
||||
grafana.ini: |
|
||||
app_mode = development
|
||||
target = zanzana-server
|
||||
|
||||
[log]
|
||||
level = info
|
||||
|
||||
[database]
|
||||
type = postgres
|
||||
host = postgres.default.svc:5432
|
||||
@@ -103,17 +99,27 @@ data:
|
||||
|
||||
[feature_toggles]
|
||||
zanzana = true
|
||||
authZGRPCServer = true
|
||||
|
||||
[zanzana.server]
|
||||
allow_insecure = true
|
||||
check_query_cache = true
|
||||
http_addr = 0.0.0.0:8080
|
||||
|
||||
[grpc_server]
|
||||
enabled = true
|
||||
address = 0.0.0.0:50051
|
||||
enable_logging = true
|
||||
use_tls = false
|
||||
|
||||
[log]
|
||||
level = debug
|
||||
|
||||
[server]
|
||||
http_port = 8080
|
||||
|
||||
[zanzana.server]
|
||||
check_query_cache = true
|
||||
check_query_cache_enabled = true
|
||||
check_query_cache_ttl = 10s
|
||||
list_objects_deadline = 3s
|
||||
list_objects_max_results = 1000
|
||||
signing_keys_url = http://host.docker.internal:8080/v1/keys
|
||||
use_streamed_list_objects = false
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -124,10 +130,10 @@ metadata:
|
||||
name: zanzana
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
- name: zanzana-http-metrics
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
- name: grpc
|
||||
- name: zanzana-grpc
|
||||
port: 50051
|
||||
targetPort: 50051
|
||||
selector:
|
||||
@@ -159,19 +165,26 @@ spec:
|
||||
env:
|
||||
- name: GF_PATHS_CONFIG
|
||||
value: /etc/grafana-config/grafana.ini
|
||||
image: grafana/grafana-oss-dev:12.2.0-257970
|
||||
image: grafana/grafana-dev:12.2.0-257970
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: zanzana
|
||||
ports:
|
||||
- containerPort: 50051
|
||||
name: grpc
|
||||
- containerPort: 8080
|
||||
name: http-metrics
|
||||
readinessProbe:
|
||||
grpc:
|
||||
port: 50051
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
volumeMounts:
|
||||
- mountPath: /etc/grafana-config
|
||||
name: zanzana-config
|
||||
- mountPath: /var/lib/grafana
|
||||
name: zanzana-storage
|
||||
serviceAccount: zanzana
|
||||
volumes:
|
||||
- configMap:
|
||||
name: zanzana-config
|
||||
name: zanzana-config
|
||||
- emptyDir: {}
|
||||
name: zanzana-storage
|
||||
---
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
@@ -72,30 +73,202 @@ var RoleInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
},
|
||||
)
|
||||
|
||||
var ResourcePermissionInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
"resourcepermissions", "resourcepermission", "ResourcePermission",
|
||||
func() runtime.Object { return &ResourcePermission{} },
|
||||
func() runtime.Object { return &ResourcePermissionList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
perm, ok := obj.(*ResourcePermission)
|
||||
if ok {
|
||||
if perm != nil {
|
||||
return []interface{}{
|
||||
perm.Name,
|
||||
perm.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("expected resource permission")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var userKind = UserKind()
|
||||
var UserResourceInfo = utils.NewResourceInfo(userKind.Group(), userKind.Version(),
|
||||
userKind.GroupVersionResource().Resource, strings.ToLower(userKind.Kind()), userKind.Kind(),
|
||||
func() runtime.Object { return userKind.ZeroValue() },
|
||||
func() runtime.Object { return userKind.ZeroListValue() },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Login", Type: "string", Format: "string", Description: "The user login"},
|
||||
{Name: "Email", Type: "string", Format: "string", Description: "The user email"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
u, ok := obj.(*User)
|
||||
if ok {
|
||||
return []interface{}{
|
||||
u.Name,
|
||||
u.Spec.Login,
|
||||
u.Spec.Email,
|
||||
u.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected user")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var teamKind = TeamKind()
|
||||
var TeamResourceInfo = utils.NewResourceInfo(teamKind.Group(), teamKind.Version(),
|
||||
teamKind.GroupVersionResource().Resource, strings.ToLower(teamKind.Kind()), teamKind.Kind(),
|
||||
func() runtime.Object { return teamKind.ZeroValue() },
|
||||
func() runtime.Object { return teamKind.ZeroListValue() },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Title", Type: "string", Format: "string", Description: "The team name"},
|
||||
{Name: "Email", Type: "string", Format: "string", Description: "team email"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*Team)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected team")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.Spec.Title,
|
||||
m.Spec.Email,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var serviceAccountKind = ServiceAccountKind()
|
||||
var ServiceAccountResourceInfo = utils.NewResourceInfo(serviceAccountKind.Group(), serviceAccountKind.Version(),
|
||||
serviceAccountKind.GroupVersionResource().Resource, strings.ToLower(serviceAccountKind.Kind()), serviceAccountKind.Kind(),
|
||||
func() runtime.Object { return serviceAccountKind.ZeroValue() },
|
||||
func() runtime.Object { return serviceAccountKind.ZeroListValue() },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Title", Type: "string", Format: "string"},
|
||||
{Name: "Disabled", Type: "boolean"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
sa, ok := obj.(*ServiceAccount)
|
||||
if ok {
|
||||
return []interface{}{
|
||||
sa.Name,
|
||||
sa.Spec.Title,
|
||||
sa.Spec.Disabled,
|
||||
sa.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected service account")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var teamBindingKind = TeamBindingKind()
|
||||
var TeamBindingResourceInfo = utils.NewResourceInfo(
|
||||
teamBindingKind.Group(), teamBindingKind.Version(),
|
||||
teamBindingKind.GroupVersionResource().Resource,
|
||||
strings.ToLower(teamBindingKind.Kind()), teamBindingKind.Kind(),
|
||||
func() runtime.Object { return teamBindingKind.ZeroValue() },
|
||||
func() runtime.Object { return teamBindingKind.ZeroListValue() },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Team", Type: "string"},
|
||||
{Name: "Created At", Type: "string", Format: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*TeamBinding)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected team binding")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.Spec.TeamRef.Name,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
schemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
|
||||
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
|
||||
InternalGroupVersion = schema.GroupVersion{Group: GROUP, Version: runtime.APIVersionInternal}
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
localSchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
err := AddAuthZKnownTypes(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = AddAuthNKnownTypes(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metav1.AddToGroupVersion(s, SchemeGroupVersion)
|
||||
return nil
|
||||
}, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(schemeGroupVersion,
|
||||
func AddAuthZKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&CoreRole{},
|
||||
&CoreRoleList{},
|
||||
&Role{},
|
||||
&RoleList{},
|
||||
&ResourcePermission{},
|
||||
&ResourcePermissionList{},
|
||||
|
||||
// What is this about?
|
||||
&metav1.PartialObjectMetadata{},
|
||||
&metav1.PartialObjectMetadataList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddAuthNKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
// Identity
|
||||
&User{},
|
||||
&UserList{},
|
||||
&ServiceAccount{},
|
||||
&ServiceAccountList{},
|
||||
&Team{},
|
||||
&TeamList{},
|
||||
&TeamBinding{},
|
||||
&TeamBindingList{},
|
||||
// For now these are registered in pkg/apis/iam/v0alpha1/register.go
|
||||
// &UserTeamList{},
|
||||
// &ServiceAccountTokenList{},
|
||||
// &DisplayList{},
|
||||
// &SSOSetting{},
|
||||
// &SSOSettingList{},
|
||||
// &TeamMemberList{},
|
||||
|
||||
&metav1.PartialObjectMetadata{},
|
||||
&metav1.PartialObjectMetadataList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,15 +23,13 @@ type ResourcePermissionspecPermission struct {
|
||||
Kind ResourcePermissionSpecPermissionKind `json:"kind"`
|
||||
// uid of the identity getting the permission
|
||||
Name string `json:"name"`
|
||||
// list of actions granted to the user (e.g. "admin" or "get", "update")
|
||||
Verbs []string `json:"verbs"`
|
||||
// action set granted to the user (e.g. "admin" or "edit", "view")
|
||||
Verb string `json:"verb"`
|
||||
}
|
||||
|
||||
// NewResourcePermissionspecPermission creates a new ResourcePermissionspecPermission object.
|
||||
func NewResourcePermissionspecPermission() *ResourcePermissionspecPermission {
|
||||
return &ResourcePermissionspecPermission{
|
||||
Verbs: []string{},
|
||||
}
|
||||
return &ResourcePermissionspecPermission{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
@@ -1111,23 +1111,16 @@ func schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref common.Re
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"verbs": {
|
||||
"verb": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "list of actions granted to the user (e.g. \"admin\" or \"get\", \"update\")",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Description: "action set granted to the user (e.g. \"admin\" or \"edit\", \"view\")",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"kind", "name", "verbs"},
|
||||
Required: []string{"kind", "name", "verb"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ func New(cfg app.Config) (app.App, error) {
|
||||
|
||||
logging.DefaultLogger.Info("FolderReconciler created")
|
||||
|
||||
reconcilerOptions := simple.BasicReconcileOptions{}
|
||||
|
||||
if cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace != "" {
|
||||
reconcilerOptions.Namespace = cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace
|
||||
}
|
||||
|
||||
config := simple.AppConfig{
|
||||
Name: cfg.ManifestData.AppName,
|
||||
KubeConfig: cfg.KubeConfig,
|
||||
@@ -54,11 +60,9 @@ func New(cfg app.Config) (app.App, error) {
|
||||
},
|
||||
UnmanagedKinds: []simple.AppUnmanagedKind{
|
||||
{
|
||||
Kind: foldersKind.FolderKind(),
|
||||
Reconciler: folderReconciler,
|
||||
ReconcileOptions: simple.BasicReconcileOptions{
|
||||
Namespace: cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace,
|
||||
},
|
||||
Kind: foldersKind.FolderKind(),
|
||||
Reconciler: folderReconciler,
|
||||
ReconcileOptions: reconcilerOptions,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.40.3
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
|
||||
k8s.io/apimachinery v0.33.3
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -34,11 +36,13 @@ require (
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
@@ -54,7 +58,6 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/client-go v0.33.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
|
||||
@@ -36,6 +36,8 @@ github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhck
|
||||
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
||||
@@ -10,7 +10,8 @@ manifest: {
|
||||
go: {enabled: true}
|
||||
}
|
||||
kinds: [
|
||||
preferencesv1alpha1,
|
||||
preferencesV1alpha1,
|
||||
starsV1alpha1,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package preferences
|
||||
|
||||
preferencesv1alpha1: {
|
||||
preferencesV1alpha1: {
|
||||
kind: "Preferences"
|
||||
pluralName: "Preferences"
|
||||
scope: "Namespaced"
|
||||
|
||||
validation: {
|
||||
operations: [
|
||||
"CREATE",
|
||||
"UPDATE",
|
||||
]
|
||||
}
|
||||
schema: {
|
||||
spec: {
|
||||
// UID for the home dashboard
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package preferences
|
||||
|
||||
starsV1alpha1: {
|
||||
kind: "Stars"
|
||||
pluralName: "Stars"
|
||||
scope: "Namespaced"
|
||||
|
||||
validation: {
|
||||
operations: [
|
||||
"CREATE",
|
||||
"UPDATE",
|
||||
]
|
||||
}
|
||||
schema: {
|
||||
#Resource: {
|
||||
group: string
|
||||
kind: string
|
||||
|
||||
// The set of resources
|
||||
// +listType=set
|
||||
names: [...string]
|
||||
}
|
||||
spec: {
|
||||
resource: [...#Resource]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
time "time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
var PreferencesResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
|
||||
"preferences", "preferences", "Preferences",
|
||||
func() runtime.Object { return &Preferences{} },
|
||||
func() runtime.Object { return &PreferencesList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]any, error) {
|
||||
p, ok := obj.(*Preferences)
|
||||
if ok && p != nil {
|
||||
return []any{
|
||||
p.Name,
|
||||
p.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected preferences")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
|
||||
"stars", "stars", "Stars",
|
||||
func() runtime.Object { return &Stars{} },
|
||||
func() runtime.Object { return &StarsList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
Reader: func(obj any) ([]any, error) {
|
||||
p, ok := obj.(*Stars)
|
||||
if ok && p != nil {
|
||||
return []any{
|
||||
p.Name,
|
||||
p.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected stars")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
schemeGroupVersion = GroupVersion
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(schemeGroupVersion,
|
||||
&Preferences{},
|
||||
&PreferencesList{},
|
||||
&Stars{},
|
||||
&StarsList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return nil // return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// StarsJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
|
||||
type StarsJSONCodec struct{}
|
||||
|
||||
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
|
||||
func (*StarsJSONCodec) Read(reader io.Reader, into resource.Object) error {
|
||||
return json.NewDecoder(reader).Decode(into)
|
||||
}
|
||||
|
||||
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
|
||||
func (*StarsJSONCodec) Write(writer io.Writer, from resource.Object) error {
|
||||
return json.NewEncoder(writer).Encode(from)
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Codec = &StarsJSONCodec{}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
type StarsMetadata struct {
|
||||
UpdateTimestamp time.Time `json:"updateTimestamp"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Uid string `json:"uid"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
|
||||
Finalizers []string `json:"finalizers"`
|
||||
ResourceVersion string `json:"resourceVersion"`
|
||||
Generation int64 `json:"generation"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// NewStarsMetadata creates a new StarsMetadata object.
|
||||
func NewStarsMetadata() *StarsMetadata {
|
||||
return &StarsMetadata{
|
||||
Finalizers: []string{},
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Stars struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
|
||||
|
||||
// Spec is the spec of the Stars
|
||||
Spec StarsSpec `json:"spec" yaml:"spec"`
|
||||
|
||||
Status StarsStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
func (o *Stars) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
func (o *Stars) SetSpec(spec any) error {
|
||||
cast, ok := spec.(StarsSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
|
||||
}
|
||||
o.Spec = cast
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Stars) GetSubresources() map[string]any {
|
||||
return map[string]any{
|
||||
"status": o.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) GetSubresource(name string) (any, bool) {
|
||||
switch name {
|
||||
case "status":
|
||||
return o.Status, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) SetSubresource(name string, value any) error {
|
||||
switch name {
|
||||
case "status":
|
||||
cast, ok := value.(StarsStatus)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set status type %#v, not of type StarsStatus", value)
|
||||
}
|
||||
o.Status = cast
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("subresource '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) GetStaticMetadata() resource.StaticMetadata {
|
||||
gvk := o.GroupVersionKind()
|
||||
return resource.StaticMetadata{
|
||||
Name: o.ObjectMeta.Name,
|
||||
Namespace: o.ObjectMeta.Namespace,
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) SetStaticMetadata(metadata resource.StaticMetadata) {
|
||||
o.Name = metadata.Name
|
||||
o.Namespace = metadata.Namespace
|
||||
o.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: metadata.Group,
|
||||
Version: metadata.Version,
|
||||
Kind: metadata.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Stars) GetCommonMetadata() resource.CommonMetadata {
|
||||
dt := o.DeletionTimestamp
|
||||
var deletionTimestamp *time.Time
|
||||
if dt != nil {
|
||||
deletionTimestamp = &dt.Time
|
||||
}
|
||||
// Legacy ExtraFields support
|
||||
extraFields := make(map[string]any)
|
||||
if o.Annotations != nil {
|
||||
extraFields["annotations"] = o.Annotations
|
||||
}
|
||||
if o.ManagedFields != nil {
|
||||
extraFields["managedFields"] = o.ManagedFields
|
||||
}
|
||||
if o.OwnerReferences != nil {
|
||||
extraFields["ownerReferences"] = o.OwnerReferences
|
||||
}
|
||||
return resource.CommonMetadata{
|
||||
UID: string(o.UID),
|
||||
ResourceVersion: o.ResourceVersion,
|
||||
Generation: o.Generation,
|
||||
Labels: o.Labels,
|
||||
CreationTimestamp: o.CreationTimestamp.Time,
|
||||
DeletionTimestamp: deletionTimestamp,
|
||||
Finalizers: o.Finalizers,
|
||||
UpdateTimestamp: o.GetUpdateTimestamp(),
|
||||
CreatedBy: o.GetCreatedBy(),
|
||||
UpdatedBy: o.GetUpdatedBy(),
|
||||
ExtraFields: extraFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) SetCommonMetadata(metadata resource.CommonMetadata) {
|
||||
o.UID = types.UID(metadata.UID)
|
||||
o.ResourceVersion = metadata.ResourceVersion
|
||||
o.Generation = metadata.Generation
|
||||
o.Labels = metadata.Labels
|
||||
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
|
||||
if metadata.DeletionTimestamp != nil {
|
||||
dt := metav1.NewTime(*metadata.DeletionTimestamp)
|
||||
o.DeletionTimestamp = &dt
|
||||
} else {
|
||||
o.DeletionTimestamp = nil
|
||||
}
|
||||
o.Finalizers = metadata.Finalizers
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string)
|
||||
}
|
||||
if !metadata.UpdateTimestamp.IsZero() {
|
||||
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
|
||||
}
|
||||
if metadata.CreatedBy != "" {
|
||||
o.SetCreatedBy(metadata.CreatedBy)
|
||||
}
|
||||
if metadata.UpdatedBy != "" {
|
||||
o.SetUpdatedBy(metadata.UpdatedBy)
|
||||
}
|
||||
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
|
||||
if metadata.ExtraFields != nil {
|
||||
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
|
||||
if cast, ok := annotations.(map[string]string); ok {
|
||||
o.Annotations = cast
|
||||
}
|
||||
}
|
||||
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
|
||||
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
|
||||
o.ManagedFields = cast
|
||||
}
|
||||
}
|
||||
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
|
||||
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
|
||||
o.OwnerReferences = cast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Stars) GetCreatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
|
||||
}
|
||||
|
||||
func (o *Stars) SetCreatedBy(createdBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
|
||||
}
|
||||
|
||||
func (o *Stars) GetUpdateTimestamp() time.Time {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (o *Stars) SetUpdateTimestamp(updateTimestamp time.Time) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o *Stars) GetUpdatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
|
||||
}
|
||||
|
||||
func (o *Stars) SetUpdatedBy(updatedBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
|
||||
}
|
||||
|
||||
func (o *Stars) Copy() resource.Object {
|
||||
return resource.CopyObject(o)
|
||||
}
|
||||
|
||||
func (o *Stars) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *Stars) DeepCopy() *Stars {
|
||||
cpy := &Stars{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *Stars) DeepCopyInto(dst *Stars) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
|
||||
o.Spec.DeepCopyInto(&dst.Spec)
|
||||
o.Status.DeepCopyInto(&dst.Status)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.Object = &Stars{}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []Stars `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func (o *StarsList) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *StarsList) Copy() resource.ListObject {
|
||||
cpy := &StarsList{
|
||||
TypeMeta: o.TypeMeta,
|
||||
Items: make([]Stars, len(o.Items)),
|
||||
}
|
||||
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
if item, ok := o.Items[i].Copy().(*Stars); ok {
|
||||
cpy.Items[i] = *item
|
||||
}
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *StarsList) GetItems() []resource.Object {
|
||||
items := make([]resource.Object, len(o.Items))
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
items[i] = &o.Items[i]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (o *StarsList) SetItems(items []resource.Object) {
|
||||
o.Items = make([]Stars, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
o.Items[i] = *items[i].(*Stars)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *StarsList) DeepCopy() *StarsList {
|
||||
cpy := &StarsList{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *StarsList) DeepCopyInto(dst *StarsList) {
|
||||
resource.CopyObjectInto(dst, o)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.ListObject = &StarsList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
// DeepCopy creates a full deep copy of Spec
|
||||
func (s *StarsSpec) DeepCopy() *StarsSpec {
|
||||
cpy := &StarsSpec{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies Spec into another Spec object
|
||||
func (s *StarsSpec) DeepCopyInto(dst *StarsSpec) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
|
||||
// DeepCopy creates a full deep copy of StarsStatus
|
||||
func (s *StarsStatus) DeepCopy() *StarsStatus {
|
||||
cpy := &StarsStatus{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies StarsStatus into another StarsStatus object
|
||||
func (s *StarsStatus) DeepCopyInto(dst *StarsStatus) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaStars = resource.NewSimpleSchema("preferences.grafana.app", "v1alpha1", &Stars{}, &StarsList{}, resource.WithKind("Stars"),
|
||||
resource.WithPlural("stars"), resource.WithScope(resource.NamespacedScope))
|
||||
kindStars = resource.Kind{
|
||||
Schema: schemaStars,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
resource.KindEncodingJSON: &StarsJSONCodec{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Kind returns a resource.Kind for this Schema with a JSON codec
|
||||
func StarsKind() resource.Kind {
|
||||
return kindStars
|
||||
}
|
||||
|
||||
// Schema returns a resource.SimpleSchema representation of Stars
|
||||
func StarsSchema() *resource.SimpleSchema {
|
||||
return schemaStars
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Schema = kindStars
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsResource struct {
|
||||
Group string `json:"group"`
|
||||
Kind string `json:"kind"`
|
||||
// The set of resources
|
||||
// +listType=set
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
|
||||
// NewStarsResource creates a new StarsResource object.
|
||||
func NewStarsResource() *StarsResource {
|
||||
return &StarsResource{
|
||||
Names: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsSpec struct {
|
||||
Resource []StarsResource `json:"resource"`
|
||||
}
|
||||
|
||||
// NewStarsSpec creates a new StarsSpec object.
|
||||
func NewStarsSpec() *StarsSpec {
|
||||
return &StarsSpec{
|
||||
Resource: []StarsResource{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsstatusOperatorState struct {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
LastEvaluation string `json:"lastEvaluation"`
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
State StarsStatusOperatorStateState `json:"state"`
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
DescriptiveState *string `json:"descriptiveState,omitempty"`
|
||||
// details contains any extra information that is operator-specific
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewStarsstatusOperatorState creates a new StarsstatusOperatorState object.
|
||||
func NewStarsstatusOperatorState() *StarsstatusOperatorState {
|
||||
return &StarsstatusOperatorState{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsStatus struct {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
OperatorStates map[string]StarsstatusOperatorState `json:"operatorStates,omitempty"`
|
||||
// additionalFields is reserved for future use
|
||||
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
|
||||
}
|
||||
|
||||
// NewStarsStatus creates a new StarsStatus object.
|
||||
func NewStarsStatus() *StarsStatus {
|
||||
return &StarsStatus{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StarsStatusOperatorStateState string
|
||||
|
||||
const (
|
||||
StarsStatusOperatorStateStateSuccess StarsStatusOperatorStateState = "success"
|
||||
StarsStatusOperatorStateStateInProgress StarsStatusOperatorStateState = "in_progress"
|
||||
StarsStatusOperatorStateStateFailed StarsStatusOperatorStateState = "failed"
|
||||
)
|
||||
@@ -0,0 +1,639 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by openapi-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences": schema_pkg_apis_preferences_v1alpha1_Preferences(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences": schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesList": schema_pkg_apis_preferences_v1alpha1_PreferencesList(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec": schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus": schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars": schema_pkg_apis_preferences_v1alpha1_Stars(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsList": schema_pkg_apis_preferences_v1alpha1_StarsList(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource": schema_pkg_apis_preferences_v1alpha1_StarsResource(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec": schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus": schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref),
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_Preferences(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: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Spec is the spec of the Preferences",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec"),
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "spec", "status"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"analytics": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"performance": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"functional": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesList(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: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"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/preferences/pkg/apis/preferences/v1alpha1.Preferences"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"bookmarkUrls": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"bookmarkUrls"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"homeTab": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "one of: '' | 'query' | 'starred';",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"homeDashboardUID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "UID for the home dashboard",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"timezone": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The timezone selection",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"weekStart": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "day of the week (sunday, monday, etc)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"theme": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "light, dark, empty is default",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"language": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Selected language (beta)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"regionalFormat": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Selected locale (beta)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"queryHistory": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Explore query history preferences",
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"),
|
||||
},
|
||||
},
|
||||
"cookiePreferences": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Cookie preferences",
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences"),
|
||||
},
|
||||
},
|
||||
"navbar": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Navigation preferences",
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"operatorStates": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"additionalFields": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "additionalFields is reserved for future use",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"lastEvaluation": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "lastEvaluation is the ResourceVersion last evaluated",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"state": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"descriptiveState": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"details": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "details contains any extra information that is operator-specific",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"lastEvaluation", "state"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_Stars(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: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Spec is the spec of the Stars",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec"),
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "spec", "status"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_StarsList(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: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"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/preferences/pkg/apis/preferences/v1alpha1.Stars"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_StarsResource(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"group": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"names": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The set of resources",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"group", "kind", "names"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"resource": {
|
||||
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/preferences/pkg/apis/preferences/v1alpha1.StarsResource"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"resource"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"operatorStates": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"additionalFields": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "additionalFields is reserved for future use",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"lastEvaluation": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "lastEvaluation is the ResourceVersion last evaluated",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"state": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"descriptiveState": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "descriptiveState is an optional more descriptive state field which has no requirements on format",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"details": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "details contains any extra information that is operator-specific",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"lastEvaluation", "state"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesNavbarPreference,BookmarkUrls
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsSpec,Resource
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesList,ListMeta
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsList,ListMeta
|
||||
@@ -20,6 +20,9 @@ var (
|
||||
rawSchemaPreferencesv1alpha1 = []byte(`{"spec":{"properties":{"cookiePreferences":{"description":"Cookie preferences","properties":{"analytics":{"type":"object"},"functional":{"type":"object"},"performance":{"type":"object"}},"type":"object"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"description":"Navigation preferences","properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"queryHistory":{"description":"Explore query history preferences","properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
|
||||
versionSchemaPreferencesv1alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaPreferencesv1alpha1, &versionSchemaPreferencesv1alpha1)
|
||||
rawSchemaStarsv1alpha1 = []byte(`{"spec":{"properties":{"resource":{"items":{"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"type":"array"}},"required":["resource"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
|
||||
versionSchemaStarsv1alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1)
|
||||
)
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
@@ -35,7 +38,31 @@ var appManifestData = app.ManifestData{
|
||||
Plural: "Preferences",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaPreferencesv1alpha1,
|
||||
Admission: &app.AdmissionCapabilities{
|
||||
Validation: &app.ValidationCapability{
|
||||
Operations: []app.AdmissionOperation{
|
||||
app.AdmissionOperationCreate,
|
||||
app.AdmissionOperationUpdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
Schema: &versionSchemaPreferencesv1alpha1,
|
||||
},
|
||||
|
||||
{
|
||||
Kind: "Stars",
|
||||
Plural: "Stars",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Admission: &app.AdmissionCapabilities{
|
||||
Validation: &app.ValidationCapability{
|
||||
Operations: []app.AdmissionOperation{
|
||||
app.AdmissionOperationCreate,
|
||||
app.AdmissionOperationUpdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
Schema: &versionSchemaStarsv1alpha1,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -52,6 +79,7 @@ func RemoteManifest() app.Manifest {
|
||||
|
||||
var kindVersionToGoType = map[string]resource.Kind{
|
||||
"Preferences/v1alpha1": v1alpha1.PreferencesKind(),
|
||||
"Stars/v1alpha1": v1alpha1.StarsKind(),
|
||||
}
|
||||
|
||||
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/apifmt"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/apifmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -2027,6 +2027,7 @@ enable =
|
||||
# This is EXPERIMENTAL. Please, do not use this section
|
||||
enable_api = true
|
||||
provider = static
|
||||
url =
|
||||
|
||||
[feature_toggles.openfeature.context]
|
||||
# This is EXPERIMENTAL. Please, do not use this section
|
||||
|
||||
@@ -224,57 +224,19 @@ make test-go-integration-postgres
|
||||
|
||||
### Run end-to-end tests
|
||||
|
||||
Grafana uses [Cypress](https://www.cypress.io/) to end-to-end test core features. Core plugins use [Playwright](https://playwright.dev/) to run automated end-to-end tests. You can find more information on how to add end-to-end tests to your core plugin [in our end-to-end testing style guide](./style-guides/e2e-plugins.md)
|
||||
- Grafana uses [Playwright](https://playwright.dev/) to run automated end-to-end tests. You can find more information [in our end-to-end testing style guide](./style-guides/e2e-playwright.md#playwright-for-plugins)
|
||||
|
||||
#### Run Cypress tests
|
||||
- Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers: `yarn playwright install chromium`.
|
||||
- Run tests with `yarn e2e:playwright [optional path to test file]`.
|
||||
|
||||
To run all tests in a headless Chromium browser.
|
||||
- To open the last HTML report, you can run `yarn playwright show-report`. You can also open an arbitrary report with `yarn playwright show-report <reportLocation>`. The reports are also downloadable from CI by:
|
||||
- Clicking through to _End-to-end tests_/_All Playwright tests complete_.
|
||||
- Clicking _Summary_.
|
||||
- Download the _playwright-html-<number>_ artifact.
|
||||
- Unzip.
|
||||
- Run `yarn playwright show-report <reportLocation>`
|
||||
|
||||
```
|
||||
yarn e2e
|
||||
```
|
||||
|
||||
By default, the end-to-end tests start a Grafana instance listening on `localhost:3001`. To use a different URL, set the `BASE_URL` environment variable:
|
||||
|
||||
```
|
||||
BASE_URL=http://localhost:3333 yarn e2e
|
||||
```
|
||||
|
||||
To follow all tests in the browser while they're running, use `yarn e2e:debug`
|
||||
|
||||
```
|
||||
yarn e2e:debug
|
||||
```
|
||||
|
||||
To choose a single test to follow in the browser as it runs, use `yarn e2e:dev`
|
||||
|
||||
```
|
||||
yarn e2e:dev
|
||||
```
|
||||
|
||||
#### To run the Playwright tests:
|
||||
|
||||
**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to use reports to analyze failing tests, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode).
|
||||
|
||||
Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers.
|
||||
|
||||
```
|
||||
yarn playwright install chromium
|
||||
```
|
||||
|
||||
The following script starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) (same server that is being used when running e2e tests in CI) on port 3001 and runs all the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps.
|
||||
|
||||
```
|
||||
yarn e2e:playwright
|
||||
```
|
||||
|
||||
You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable:
|
||||
|
||||
```
|
||||
GRAFANA_URL=http://localhost:3000 yarn e2e:playwright
|
||||
```
|
||||
|
||||
Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL.
|
||||
If you are curious about other commands, you can see the full list in [the Playwright documentation](https://playwright.dev/docs/test-cli#all-options).
|
||||
|
||||
## Configure Grafana for development
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# End-to-end tests for core Grafana
|
||||
|
||||
This guide explains how to conduct end-to-end tests for the [Grafana repository](https://github.com/grafana/grafana). Ensure that you've read the [generalized E2E document](e2e.md).
|
||||
|
||||
## Commands
|
||||
|
||||
- `yarn e2e`: Creates an isolated `grafana-server` home under `\<repo-root>/e2e/tmp` with provisioned data sources and dashboards. This makes a local copy of the build binary and frontend assets from your repository root so you need to have a built backend and frontend. The server starts on port `3001` so it does not conflict with your normal dev server.
|
||||
- `yarn e2e:debug`: Same as previous but runs the tests in Chrome and doesn't shut down after completion.
|
||||
- `yarn e2e:dev`: Same as previous but does not run any tests on startup. It lets you pick a test first.
|
||||
|
||||
If you already have a Grafana instance running, you can provide a specific URL by setting the `BASE_URL` environment variable:
|
||||
|
||||
```shell
|
||||
BASE_URL=http://172.0.10.2:3333 yarn e2e
|
||||
```
|
||||
|
||||
The previous commands use some `utils` scripts under [_\<repo-root>/e2e_](../../e2e) that you can also use for more control.
|
||||
|
||||
- `./scripts/grafana-server/start-server`: This creates a new Grafana server working directory, sets up configuration. and starts the server. It also kills any previously started server that is still running using the `pid` file at `\<repo-root>/scripts/grafana-server/tmp/pid`.
|
||||
- `./scripts/grafana-server/wait-for-grafana`: waits for `$HOST` and `$PORT` to be available. Per default `localhost` and `3001`.
|
||||
- `./e2e/run-suite <debug|dev|noarg>`: Starts Cypress in different modes.
|
||||
|
||||
## Test suites
|
||||
|
||||
You can find integration tests at `\<repo-root>/e2e/suite\<x>/specs`.
|
||||
@@ -0,0 +1,294 @@
|
||||
# End-to-end tests
|
||||
|
||||
Grafana Labs uses a minimal [homegrown solution](https://github.com/grafana/plugin-tools/tree/main/packages/plugin-e2e) built on top of [Playwright](https://playwright.dev/) for its end-to-end (E2E) tests.
|
||||
|
||||
Important notes:
|
||||
|
||||
- We generally store all element identifiers ([CSS selectors](https://mdn.io/docs/Web/CSS/CSS_Selectors)) within the framework for reuse and maintainability.
|
||||
- We generally do not use stubs or mocks as to fully simulate a real user.
|
||||
- We also use Playwright for the [plugins' end-to-end tests](#playwright-for-plugins).
|
||||
|
||||
## Framework structure
|
||||
|
||||
Our framework structure is inspired by [Martin Fowler's Page Object](https://martinfowler.com/bliki/PageObject.html).
|
||||
|
||||
- **`Selector`**: A unique identifier that is used from the E2E framework to retrieve an element from the browser
|
||||
- **`Page`**: An abstraction for an object that contains one or more `Selector` identifiers with the `visit` function to go to the page.
|
||||
- **`Component`**: An abstraction for an object that contains one or more `Selector` identifiers but without the `visit` function
|
||||
- **`Flow`**: An abstraction that contains a sequence of actions on one or more `Page` abstractions that can be reused and shared between tests
|
||||
|
||||
## How to run the Playwright tests:
|
||||
|
||||
**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to use reports to analyze failing tests, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode).
|
||||
|
||||
Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers.
|
||||
|
||||
```
|
||||
yarn playwright install chromium
|
||||
```
|
||||
|
||||
The following script starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) (same server that is being used when running e2e tests in CI) on port 3001 and runs all the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps.
|
||||
|
||||
```
|
||||
yarn e2e:playwright
|
||||
```
|
||||
|
||||
You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable:
|
||||
|
||||
```
|
||||
GRAFANA_URL=http://localhost:3000 yarn e2e:playwright
|
||||
```
|
||||
|
||||
Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL.
|
||||
|
||||
### Commands commonly used:
|
||||
|
||||
1 - **To open the Playwright UI**. It starts the Grafana server and then Playwright, which runs against this server.
|
||||
|
||||
```
|
||||
yarn e2e:playwright --ui
|
||||
```
|
||||
|
||||
2 - **To run an individual test**. It will run the test that matches the string passed to _grep_. Playwright will run all of them if you use a string that matches multiple tests.
|
||||
|
||||
```
|
||||
yarn e2e:playwright --grep <testname>
|
||||
```
|
||||
|
||||
You can also run all the tests matching a specific tag with _@tagName_.
|
||||
|
||||
```
|
||||
yarn e2e:playwright --grep @<tagname>
|
||||
```
|
||||
|
||||
3 - **To run a project**. It will run the entire project. You can find them in [grafana/playwright.config.ts](https://github.com/grafana/grafana/blob/main/playwright.config.ts#L90).
|
||||
|
||||
```
|
||||
yarn e2e:playwright --project <projectname>
|
||||
```
|
||||
|
||||
4- **To open the last HTML report**. It will open a Chrome window with the test list and the related info (success/error, name, time, steps, ...).
|
||||
|
||||
```
|
||||
yarn playwright show-report
|
||||
```
|
||||
|
||||
You can open an arbitrary report with `yarn playwright show-report <reportLocation>`. The reports are also downloadable from CI by:
|
||||
|
||||
- Clicking through to _End-to-end tests_/_All Playwright tests complete_.
|
||||
- Clicking _Summary_.
|
||||
- Download the _playwright-html-<number>_ artifact.
|
||||
- Unzip.
|
||||
- Run `yarn playwright show-report <reportLocation>`
|
||||
|
||||
You can see the full list in [the Playwright documentation](https://playwright.dev/docs/test-cli#all-options) if you are curious about other commands.
|
||||
|
||||
## How to create an end-to-end Playwright test
|
||||
|
||||
### Basic example
|
||||
|
||||
Let's start with a simple [JSX](https://reactjs.org/docs/introducing-jsx.html) example containing a single input field that we want to populate during our E2E test:
|
||||
|
||||
```jsx
|
||||
<input className="login-form-input" type="text" />
|
||||
```
|
||||
|
||||
It is possible to target the field with a CSS selector like `.login-form-input`. However, doing so is a brittle solution because style changes occur frequently.
|
||||
|
||||
Furthermore, there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details.
|
||||
|
||||
```jsx
|
||||
<input data-testid="Username input field" className="login-form-input" type="text" />
|
||||
```
|
||||
|
||||
The next step is to add the `Login` page to the `versionedPages` export within [_\<repo-root>/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `selectors.pages` in your IDE.
|
||||
|
||||
```typescript
|
||||
export const versionedPages = {
|
||||
[...]
|
||||
Login: {
|
||||
url: {
|
||||
[MIN_GRAFANA_VERSION]: '/login',
|
||||
},
|
||||
username: {
|
||||
'10.2.3': 'data-testid Username input field',
|
||||
[MIN_GRAFANA_VERSION]: 'Username input field',
|
||||
},
|
||||
password: {
|
||||
'10.2.3': 'data-testid Password input field',
|
||||
[MIN_GRAFANA_VERSION]: 'Password input field',
|
||||
},
|
||||
submit: {
|
||||
'10.2.3': 'data-testid Login button',
|
||||
[MIN_GRAFANA_VERSION]: 'Login button',
|
||||
},
|
||||
skip: {
|
||||
'10.2.3': 'data-testid Skip change password button',
|
||||
},
|
||||
},
|
||||
[...]
|
||||
```
|
||||
|
||||
In this example, the username selector is prefixed with `data-testid` and specifies the minimum Grafana version for which the value is valid. The prefix is a signal to the framework to look for the selector in the `data-testid` attribute.
|
||||
|
||||
Now that we have a page called `Login` in our `versionedPages` const, use it to add a selector in our HTML as shown in the following example. This page really signals to future developers that it is part of an E2E test.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
<input data-testid={selectors.pages.Login.username} className="login-form-input" type="text" />;
|
||||
```
|
||||
|
||||
The last step in our example is to use our `Login` page as part of a test.
|
||||
|
||||
- Use the `url` property whenever you call the [`page.goto()`](https://playwright.dev/docs/api/class-page#page-goto) in Playwright.
|
||||
- Access any defined selector from the `Login` page by invoking it: [`page.getByTestId()`](https://playwright.dev/docs/api/class-page#page-get-by-test-id).
|
||||
|
||||
```typescript
|
||||
test(
|
||||
'Can login successfully',
|
||||
{
|
||||
tag: ['@acceptance'],
|
||||
},
|
||||
async ({ selectors, page, grafanaAPICredentials }) => {
|
||||
test.skip(grafanaAPICredentials.password === 'admin', 'Does not run with default password');
|
||||
|
||||
await page.goto(selectors.pages.Login.url);
|
||||
|
||||
await page.getByTestId(selectors.pages.Login.username).fill(grafanaAPICredentials.user);
|
||||
await page.getByTestId(selectors.pages.Login.password).fill(grafanaAPICredentials.password);
|
||||
|
||||
await page.getByTestId(selectors.pages.Login.submit).click();
|
||||
|
||||
await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible();
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Advanced example
|
||||
|
||||
Let's take a look at an example that uses the same selector for multiple items in a list for instance. In this example app, there's a list of data sources that we want to click on during an E2E test.
|
||||
|
||||
```jsx
|
||||
<ul>
|
||||
{dataSources.map(({ id, name }) => (
|
||||
<li className="card-item-wrapper" key={id}>
|
||||
<a className="card-item" href={`datasources/edit/${id}`}>
|
||||
<div className="card-item-name">{name}</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
Like in the basic example, start by creating a page abstraction:
|
||||
|
||||
```typescript
|
||||
export const versionedPages = {
|
||||
[...]
|
||||
DataSources: {
|
||||
url: {
|
||||
[MIN_GRAFANA_VERSION]: '/datasources',
|
||||
},
|
||||
dataSources: {
|
||||
[MIN_GRAFANA_VERSION]: (dataSourceName: string) => `Data source list item ${dataSourceName}`,
|
||||
},
|
||||
},
|
||||
[...]
|
||||
};
|
||||
```
|
||||
|
||||
You might have noticed that instead of a simple string as the selector, there's a function that takes a string parameter as an argument and returns a formatted string using the argument.
|
||||
|
||||
Just as before, you need to add the `DataSources` url to the exported const `versionedPages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`.
|
||||
|
||||
The next step is to use the `dataSources` selector function as in the following example:
|
||||
|
||||
```jsx
|
||||
<ul>
|
||||
{dataSources.map(({ id, name }) => (
|
||||
<li className="card-item-wrapper" key={id}>
|
||||
<a className="card-item" href={`datasources/edit/${id}`}>
|
||||
<div className="card-item-name" data-testid={selectors.pages.DataSources.dataSources(name)}>
|
||||
{name}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML looks like this:
|
||||
|
||||
```html
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item A">A</div>
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item B">B</div>
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item C">C</div>
|
||||
```
|
||||
|
||||
Now we can write our test. The one thing that differs from the previous [basic example](#basic-example) is that we pass in which data source we want to click as an argument to the selector function:
|
||||
|
||||
```typescript
|
||||
test(
|
||||
'List test clicks on data source named B',
|
||||
{
|
||||
tag: ['@datasources'],
|
||||
},
|
||||
async ({ selectors, page }) => {
|
||||
await page.goto(selectors.pages.Datasources.url);
|
||||
// Select datasource
|
||||
const dataSource = 'B';
|
||||
await page.getByTextId(dataSource).click();
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Playwright for plugins
|
||||
|
||||
When end-to-end testing Grafana plugins, a best practice is to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. The `@grafana/plugin-e2e` tool extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers. Use it to enable comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana.
|
||||
|
||||
> **Note:** To learn more, refer to our documentation on [plugin development](https://grafana.com/developers/plugin-tools/) and [end-to-end plugin testing](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started).
|
||||
|
||||
## Add end-to-end tests for a core plugin
|
||||
|
||||
You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory.
|
||||
|
||||
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept.
|
||||
|
||||
1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
|
||||
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests.
|
||||
Add `'authenticate'` to the list of dependencies and specify `'playwright/.auth/admin.json'` as the storage state to ensure that all tests in your project will start already authenticated as an admin user. If you want to use a different role for and perhaps test RBAC for some of your tests, refer to our [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication).
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'mysql',
|
||||
testDir: path.join(testDirRoot, '/mysql'),
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'playwright/.auth/admin.json',
|
||||
},
|
||||
dependencies: ['authenticate'],
|
||||
},
|
||||
```
|
||||
|
||||
1. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1.
|
||||
|
||||
## Commands
|
||||
|
||||
- `yarn e2e:playwright` runs all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a specific project.
|
||||
|
||||
The `yarn e2e:playwright` command starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and runs the Playwright tests.
|
||||
|
||||
You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable:
|
||||
|
||||
`GRAFANA_URL=http://localhost:3000 yarn e2e:playwright`
|
||||
|
||||
Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL.
|
||||
|
||||
- You can provision the development server with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources, and apps.
|
||||
|
||||
> ### Playwright tests in Grafana Enterprise
|
||||
>
|
||||
> We are currently working on make Playwright available for creating end-to-end tests in [grafana-enterprise](https://github.com/grafana/grafana-enterprise).
|
||||
@@ -1,43 +0,0 @@
|
||||
# End-to-end tests for plugins
|
||||
|
||||
When end-to-end testing Grafana plugins, a best practice is to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. The `@grafana/plugin-e2e` tool extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers. Use it to enable comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana.
|
||||
|
||||
> **Note:** To learn more, refer to our documentation on [plugin development](https://grafana.com/developers/plugin-tools/) and [end-to-end plugin testing](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started).
|
||||
|
||||
## Add end-to-end tests for a core plugin
|
||||
|
||||
You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory.
|
||||
|
||||
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept.
|
||||
|
||||
1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
|
||||
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests.
|
||||
Add `'authenticate'` to the list of dependencies and specify `'playwright/.auth/admin.json'` as the storage state to ensure that all tests in your project will start already authenticated as an admin user. If you want to use a different role for and perhaps test RBAC for some of your tests, refer to our [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication).
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'mysql',
|
||||
testDir: path.join(testDirRoot, '/mysql'),
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'playwright/.auth/admin.json',
|
||||
},
|
||||
dependencies: ['authenticate'],
|
||||
},
|
||||
```
|
||||
|
||||
1. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1.
|
||||
|
||||
## Commands
|
||||
|
||||
- `yarn e2e:playwright` runs all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a specific project.
|
||||
|
||||
The `yarn e2e:playwright` command starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and runs the Playwright tests.
|
||||
|
||||
You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable:
|
||||
|
||||
`GRAFANA_URL=http://localhost:3000 yarn e2e:playwright`
|
||||
|
||||
Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL.
|
||||
|
||||
- You can provision the development server with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources, and apps.
|
||||
@@ -1,194 +0,0 @@
|
||||
# End-to-end tests
|
||||
|
||||
Grafana Labs uses a minimal [homegrown solution](../../e2e/utils/index.ts) built on top of [Cypress](https://cypress.io) for its end-to-end (E2E) tests.
|
||||
|
||||
Important notes:
|
||||
|
||||
- We generally store all element identifiers ([CSS selectors](https://mdn.io/docs/Web/CSS/CSS_Selectors)) within the framework for reuse and maintainability.
|
||||
- We generally do not use stubs or mocks as to fully simulate a real user.
|
||||
- Cypress' promises [don't behave as you might expect](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Mixing-Async-and-Sync-code).
|
||||
- [Testing core Grafana](e2e-core.md) is different than [testing plugins](e2e-plugins.md)—core Grafana uses Cypress whereas plugins use [Playwright test](https://playwright.dev/).
|
||||
|
||||
## Framework structure
|
||||
|
||||
Our framework structure is inspired by [Martin Fowler's Page Object](https://martinfowler.com/bliki/PageObject.html).
|
||||
|
||||
- **`Selector`**: A unique identifier that is used from the E2E framework to retrieve an element from the browser
|
||||
- **`Page`**: An abstraction for an object that contains one or more `Selector` identifiers with the `visit` function to go to the page.
|
||||
- **`Component`**: An abstraction for an object that contains one or more `Selector` identifiers but without the `visit` function
|
||||
- **`Flow`**: An abstraction that contains a sequence of actions on one or more `Page` abstractions that can be reused and shared between tests
|
||||
|
||||
## Basic example
|
||||
|
||||
Let's start with a simple [JSX](https://reactjs.org/docs/introducing-jsx.html) example containing a single input field that we want to populate during our E2E test:
|
||||
|
||||
```jsx
|
||||
<input className="gf-form-input login-form-input" type="text" />
|
||||
```
|
||||
|
||||
It is possible to target the field with a CSS selector like `.gf-form-input.login-form-input`. However, doing so is a brittle solution because style changes occur frequently.
|
||||
|
||||
Furthermore, there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details.
|
||||
|
||||
```jsx
|
||||
<input data-testid="Username input field" className="gf-form-input login-form-input" type="text" />
|
||||
```
|
||||
|
||||
The next step is to create a `Page` representation in our E2E framework. Doing so glues the test with the real implementation using the `pageFactory` function. For that function we can supply a `url` and selector like in the following example:
|
||||
|
||||
```typescript
|
||||
export const Login = {
|
||||
// Called via `Login.visit()`
|
||||
url: '/login',
|
||||
// Called via `Login.username()`
|
||||
username: 'data-testid Username input field',
|
||||
};
|
||||
```
|
||||
|
||||
In this example, the selector is prefixed with `data-testid`. The prefix is a signal to the framework to look for the selector in the `data-testid` attribute.
|
||||
|
||||
The next step is to add the `Login` page to the `Pages` export within [_\<repo-root>/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `e2e.pages` in your IDE.
|
||||
|
||||
```typescript
|
||||
export const Pages = {
|
||||
Login,
|
||||
…,
|
||||
…,
|
||||
…,
|
||||
};
|
||||
```
|
||||
|
||||
Now that we have a page called `Login` in our `Pages` const, use it to add a selector in our HTML as shown in the following example. This page really signals to future developers that it is part of an E2E test.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
<input data-testid={selectors.pages.Login.username} className="gf-form-input login-form-input" type="text" />;
|
||||
```
|
||||
|
||||
The last step in our example is to use our `Login` page as part of a test.
|
||||
|
||||
- Use the `url` property whenever you call the `visit` function. It is equivalent to the [`cy.visit()`](https://docs.cypress.io/api/commands/visit.html#Syntax) in Cypress.
|
||||
- Access any defined selector from the `Login` page by invoking it. This is equivalent to the result of the Cypress function [`cy.get(…)`](https://docs.cypress.io/api/commands/get.html#Syntax).
|
||||
|
||||
```typescript
|
||||
describe('Login test', () => {
|
||||
it('passes', () => {
|
||||
e2e.pages.Login.visit();
|
||||
// To prevent flaky tests, always do a `.should` on any selector that you expect to be in the DOM.
|
||||
// Read more here: https://docs.cypress.io/guides/core-concepts/retry-ability.html#Commands-vs-assertions
|
||||
e2e.pages.Login.username().should('be.visible').type('admin');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced example
|
||||
|
||||
Let's take a look at an example that uses the same selector for multiple items in a list for instance. In this example app, there's a list of data sources that we want to click on during an E2E test.
|
||||
|
||||
```jsx
|
||||
<ul>
|
||||
{dataSources.map(({ id, name }) => (
|
||||
<li className="card-item-wrapper" key={id}>
|
||||
<a className="card-item" href={`datasources/edit/${id}`}>
|
||||
<div className="card-item-name">{name}</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
Like in the basic example, start by creating a page abstraction using the `pageFactory` function:
|
||||
|
||||
```typescript
|
||||
export const DataSources = {
|
||||
url: '/datasources',
|
||||
dataSources: (dataSourceName: string) => `data-testid Data source list item ${dataSourceName}`,
|
||||
};
|
||||
```
|
||||
|
||||
You might have noticed that instead of a simple string as the selector, there's a function that takes a string parameter as an argument and returns a formatted string using the argument.
|
||||
|
||||
Just as before, you need to add the `DataSources` page to the exported const `Pages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`.
|
||||
|
||||
The next step is to use the `dataSources` selector function as in the following example:
|
||||
|
||||
```jsx
|
||||
<ul>
|
||||
{dataSources.map(({ id, name }) => (
|
||||
<li className="card-item-wrapper" key={id}>
|
||||
<a className="card-item" href={`datasources/edit/${id}`}>
|
||||
<div className="card-item-name" data-testid={selectors.pages.DataSources.dataSources(name)}>
|
||||
{name}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
```
|
||||
|
||||
When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML looks like this:
|
||||
|
||||
```html
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item A">A</div>
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item B">B</div>
|
||||
<div class="card-item-name" data-testid="data-testid Data source list item C">C</div>
|
||||
```
|
||||
|
||||
Now we can write our test. The one thing that differs from the previous [basic example](#basic-example) is that we pass in which data source we want to click as an argument to the selector function:
|
||||
|
||||
```typescript
|
||||
describe('List test', () => {
|
||||
it('clicks on data source named B', () => {
|
||||
e2e.pages.DataSources.visit();
|
||||
// To prevent flaky tests, always do a .should on any selector that you expect to be in the DOM.
|
||||
// Read more here: https://docs.cypress.io/guides/core-concepts/retry-ability.html#Commands-vs-assertions
|
||||
e2e.pages.DataSources.dataSources('B').should('be.visible').click();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## aria-label versus data-testid
|
||||
|
||||
Our selectors are set up to work with both `aria-label` attributes and `data-testid` attributes. The `aria-label` attributes help assistive technologies such as screen readers identify interactive elements of a page for our users.
|
||||
|
||||
A good example of a time to use an aria-label might be if you have a button with an **X** to close:
|
||||
|
||||
```
|
||||
<button aria-label="close">X<button>
|
||||
```
|
||||
|
||||
It might be clear visually that the **X** closes the modal, but audibly it would not be clear, for example.
|
||||
|
||||
```
|
||||
<button aria-label="close">Close<button>
|
||||
```
|
||||
|
||||
The example might read aloud to a user as "Close, Close" or something similar.
|
||||
|
||||
However, adding an aria-label to an element that is already clearly labeled or not interactive can be confusing and redundant for users with assistive technologies.
|
||||
|
||||
In such cases, don't add an unnecessary aria-label to a component so as to make them selectable for testing. Instead, use a data attribute that will not be read aloud with an assistive technology. For example:
|
||||
|
||||
```
|
||||
<button data-testid="modal-close-button">Close<button>
|
||||
```
|
||||
|
||||
We have added support for data attributes in our selectors Prefix your selector string with `data-testid`:
|
||||
|
||||
```typescript
|
||||
export const Components = {
|
||||
Login: {
|
||||
openButton: 'open-button', // this looks for an aria-label
|
||||
closeButton: 'data-testid modal-close-button', // this looks for a data-testid
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
and in your component, import the selectors and add the `data-testid`:
|
||||
|
||||
```
|
||||
<button data-testid={Selectors.Components.Login.closeButton}>
|
||||
```
|
||||
@@ -452,7 +452,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -602,7 +602,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -821,7 +821,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "auto,link,links,bg,bg-link,bg-links,color,color-link,color-links,bg-row,bg-row-link,bg-row-links,color-row,color-row-link,color-row-links,links-cellType\na,a,a,a,a,a,a,a,a,1,1,1,1,1,1,1\nb,b,b,b,b,b,b,b,b,2,2,2,2,2,2,2\nc,c,c,c,c,c,c,c,c,3,3,3,3,3,3,3\nd,d,d,d,d,d,d,d,d,4,4,4,4,4,4,4",
|
||||
@@ -984,7 +984,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "use-case,normal,color-text,color-bg,highlight\n\"color bg and apply to row\",1,0,1,1\n\"color text and apply to row\",1,1,0,1\n\"color text + color bg + apply to row\",1,1,1,1\n\"only apply to row\",1,0,0,1\n\"only color bg\",1,0,1,0\n\"only color text\",1,1,0,0\n\"color text + color bg\",1,1,1,0\n\"no colorization\",1,0,0,0",
|
||||
@@ -1132,7 +1132,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754578636810,\n 1754580436810,\n 1754582236810,\n 1754584036810,\n 1754585836810,\n 1754587636810,\n 1754589436810,\n 1754591236810,\n 1754593036810,\n 1754594836810\n ],\n [\n 68.67815082171406,\n 68.21038738705283,\n 67.85748579518524,\n 67.48088827530566,\n 67.36362085363314,\n 67.75399477824531,\n 67.9006935145112,\n 68.2893033558627,\n 68.20547872017494,\n 67.96251158324088\n ],\n [\n 67.52655915076163,\n 68.03367017865995,\n 65.44308825471086,\n 66.94142900198796,\n 66.33531057489242,\n 66.97007154625013,\n 67.88134129373145,\n 65.92102089546943,\n 65.7896781452792,\n 67.77680599320166\n ],\n [\n 68.78026730954913,\n 68.46495903270079,\n 68.29118217530531,\n 67.90561937677037,\n 68.05316512901861,\n 70.26033810424696,\n 68.05691975265877,\n 68.76874724839202,\n 70.02772232081647,\n 70.38628221361253\n ],\n [\n \"up\",\n \"down fast\",\n \"down\",\n \"down\",\n \"down\",\n \"up\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 1,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1279,7 +1279,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579008785,\n 1754580808785,\n 1754582608785,\n 1754584408785,\n 1754586208785,\n 1754588008785,\n 1754589808785,\n 1754591608785,\n 1754593408785,\n 1754595208785\n ],\n [\n 89.82213040422468,\n 89.53763263564208,\n 90.03103064454615,\n 90.51535567776907,\n 90.88960652895187,\n 91.30921636258488,\n 91.12427953645643,\n 91.1507153149385,\n 90.81021008780039,\n 91.11327042147582\n ],\n [\n 87.33191559754641,\n 88.46410173314496,\n 88.06920794235972,\n 88.98473401700574,\n 90.60349141857057,\n 90.5858441719162,\n 90.42154517148411,\n 90.32413455176213,\n 90.72055904641739,\n 88.74774424339796\n ],\n [\n 90.58845140604446,\n 90.08173133413374,\n 90.58230197880795,\n 92.55531809442493,\n 93.09165646181742,\n 93.38354884838586,\n 92.68544239576967,\n 92.15770413350477,\n 92.77335012158814,\n 92.52115072193222\n ],\n [\n \"down\",\n \"down\",\n \"up fast\",\n \"up fast\",\n \"up\",\n \"up fast\",\n \"down\",\n \"up\",\n \"down\",\n \"up\"\n ],\n [\n 2,\n 2,\n 1,\n 1,\n 1,\n 1,\n 2,\n 1,\n 2,\n 1\n ]\n ]\n }\n }\n]",
|
||||
@@ -1426,7 +1426,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1573,7 +1573,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1755,7 +1755,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1816,7 +1816,7 @@
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "a,b,c",
|
||||
@@ -1852,5 +1852,5 @@
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Table - Kitchen Sink",
|
||||
"uid": "dcb9f5e9-8066-4397-889e-864b99555dbb",
|
||||
"version": 32
|
||||
"version": 33
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,10 +68,13 @@ dc_resource("frontend-service",
|
||||
labels=["services"],
|
||||
)
|
||||
dc_resource("alloy", labels=["observability"])
|
||||
dc_resource("prometheus", labels=["observability"])
|
||||
dc_resource("loki", labels=["observability"])
|
||||
dc_resource("tempo-init", labels=["observability"])
|
||||
dc_resource("tempo", labels=["observability"])
|
||||
|
||||
dc_resource("postgres", labels=["misc"])
|
||||
dc_resource("tempo-init", labels=["misc"])
|
||||
|
||||
# paths in tilt files are confusing....
|
||||
# - if tilt is dealing the the path, it is relative to the Tiltfile
|
||||
# - if docker is dealing with the path, it is relative to the context
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//
|
||||
// Metrics
|
||||
prometheus.relabel "publish_metrics" {
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "instance"
|
||||
replacement = constants.hostname
|
||||
}
|
||||
|
||||
forward_to = [prometheus.remote_write.tilt_prom.receiver]
|
||||
}
|
||||
|
||||
prometheus.remote_write "tilt_prom" {
|
||||
endpoint {
|
||||
url = "http://prometheus:9090/api/v1/write"
|
||||
send_native_histograms = true
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Logs
|
||||
loki.relabel "publish_logs" {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//
|
||||
// Self-config
|
||||
//
|
||||
|
||||
livedebugging {
|
||||
enabled = true
|
||||
}
|
||||
@@ -21,3 +20,13 @@ loki.relabel "alloy" {
|
||||
|
||||
forward_to = [loki.relabel.publish_logs.receiver]
|
||||
}
|
||||
|
||||
// Metrics
|
||||
prometheus.exporter.self "alloy" { }
|
||||
|
||||
prometheus.scrape "alloy" {
|
||||
targets = prometheus.exporter.self.alloy.targets
|
||||
scrape_interval = "60s"
|
||||
forward_to = [prometheus.relabel.publish_metrics.receiver]
|
||||
scrape_protocols = ["PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Metrics
|
||||
prometheus.scrape "grafana_dev" {
|
||||
targets = [
|
||||
{__scheme__ = "http", __address__ = "grafana-api:3000", job = "grafana_dev", service_name = "grafana-api"},
|
||||
{__scheme__ = "http", __address__ = "frontend-service:3000", job = "grafana_dev", service_name = "frontend-service"},
|
||||
]
|
||||
scrape_interval = "15s"
|
||||
forward_to = [prometheus.relabel.publish_metrics.receiver]
|
||||
scrape_protocols = ["PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"]
|
||||
}
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
GF_DEFAULT_APP_MODE: development
|
||||
GF_PANELS_ENABLE_ALPHA: true
|
||||
GF_SERVER_CDN_URL: http://localhost:3010
|
||||
GF_FEATURE_TOGGLES_ENABLE: multiTenantFrontend
|
||||
GF_FEATURE_TOGGLES_ENABLE: multiTenantFrontend enableNativeHTTPHistogram
|
||||
GF_DATABASE_URL: postgres://grafana:grafana@postgres:5432/grafana
|
||||
GF_SERVER_ROUTER_LOGGING: true
|
||||
GF_LOG_LEVEL: info
|
||||
@@ -62,6 +62,7 @@ services:
|
||||
GF_DEFAULT_APP_MODE: development
|
||||
GF_DEFAULT_TARGET: frontend-server
|
||||
GF_SECURITY_CONTENT_SECURITY_POLICY: false
|
||||
GF_FEATURE_TOGGLES_ENABLE: enableNativeHTTPHistogram
|
||||
GF_SERVER_CDN_URL: http://localhost:3010
|
||||
GF_SERVER_ROUTER_LOGGING: true
|
||||
GF_LOG_LEVEL: info
|
||||
@@ -95,6 +96,20 @@ services:
|
||||
- /alloy-config
|
||||
depends_on:
|
||||
- loki
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
volumes:
|
||||
- prometheus-data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.enable-remote-write-receiver'
|
||||
- '--enable-feature=native-histograms'
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
loki:
|
||||
image: grafana/loki
|
||||
@@ -120,6 +135,8 @@ services:
|
||||
- tempo-data:/var/lib/tempo
|
||||
- ./configs/tempo.yaml:/etc/tempo/tempo.yaml
|
||||
command: ['-config.file=/etc/tempo/tempo.yaml']
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
depends_on:
|
||||
tempo-init:
|
||||
@@ -131,3 +148,4 @@ volumes:
|
||||
alloy-data:
|
||||
loki-data:
|
||||
tempo-data:
|
||||
prometheus-data:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,11 @@ datasources:
|
||||
uid: PD8C576611E62080A
|
||||
type: grafana-testdata-datasource
|
||||
|
||||
- name: Prometheus Pmetrics
|
||||
uid: fs-prometheus
|
||||
type: prometheus
|
||||
url: http://prometheus:9090
|
||||
|
||||
- name: Loki Logs
|
||||
uid: fs-loki
|
||||
type: loki
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"table_sparkline_cell": (import '../dev-dashboards/panel-table/table_sparkline_cell.json'),
|
||||
"table_tests": (import '../dev-dashboards/panel-table/table_tests.json'),
|
||||
"table_tests_new": (import '../dev-dashboards/panel-table/table_tests_new.json'),
|
||||
"table_v12_2_migrations": (import '../dev-dashboards/panel-table/table_v12_2_migrations.json'),
|
||||
"tall_dashboard": (import '../dev-dashboards/scenarios/tall_dashboard.json'),
|
||||
"templating-dashboard-links-and-variables": (import '../dev-dashboards/feature-templating/templating-dashboard-links-and-variables.json'),
|
||||
"templating-repeating-panels": (import '../dev-dashboards/feature-templating/templating-repeating-panels.json'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
aliases:
|
||||
- administration/cli/
|
||||
- ../cli/ # /docs/grafana/latest/cli/
|
||||
description: Guide to using grafana server cli
|
||||
keywords:
|
||||
- grafana
|
||||
@@ -12,7 +12,7 @@ labels:
|
||||
- enterprise
|
||||
- oss
|
||||
title: Grafana server CLI
|
||||
weight: 400
|
||||
weight: 1100
|
||||
---
|
||||
|
||||
# Grafana server CLI
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
aliases:
|
||||
- ../search/ # /docs/grafana/latest/search/
|
||||
description: Use the command palette to search all of Grafana and initiate key actions
|
||||
keywords:
|
||||
- search
|
||||
@@ -13,7 +15,6 @@ labels:
|
||||
- oss
|
||||
menutitle: Search
|
||||
title: Search
|
||||
weight: 80
|
||||
---
|
||||
|
||||
# Grafana search
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
---
|
||||
description: Learn how to create a Grafana-managed recording rule for the Tempo data source.
|
||||
keywords:
|
||||
- grafana
|
||||
- tempo
|
||||
- guide
|
||||
- tracing
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Create recording rules for Tempo
|
||||
title: Create Grafana-managed recording rules for Tempo
|
||||
weight: 1000
|
||||
_build:
|
||||
list: false
|
||||
---
|
||||
|
||||
# Create Grafana-managed recording rules for Tempo
|
||||
|
||||
{{< docs/private-preview product="Grafana-managed recording rules for Tempo" >}}
|
||||
|
||||
Grafana-managed recording rules let you pre-compute query results and store them as new time series metrics. These metrics can then be reused in dashboards, alerts, or downstream queries, improving performance and reliability.
|
||||
|
||||
This guide explains how to configure recording rules for the **Tempo data source**, including product-specific considerations, such as TraceQL metrics queries, time range alignment, and evaluation delays.
|
||||
|
||||
For additional information, refer to [Grafana-managed recording rules](https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules/).
|
||||
|
||||
## Create a new Tempo recording rule
|
||||
|
||||
1. In Grafana, go to **Alerts & IRM** > **Alerting** > **Alert rules**.
|
||||
|
||||
1. From the **More+** menu, select **New Grafana recording rule**.
|
||||
|
||||
1. Enter recording rule and metric name.
|
||||
|
||||
In the **New recording rule** window, configure the following:
|
||||
- **Name**: A human-readable identifier for the recording rule.
|
||||
- **Metric**: The Prometheus-compatible name of the new metric series that will be generated. For details, refer to [Prometheus metric names](https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
|
||||
- **Target data source**: Must be a **Prometheus** data source with _write_ permissions (to store the rule's results). Ensure your Prometheus or Mimir metrics backend allows Grafana to write the new series.
|
||||
|
||||
1. Define the recording rule.
|
||||
- **Data source**: Select the **Tempo** data source with _read_ permissions.
|
||||
- In the **Options** dropdown, specify a time range.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Grafana Alerting only supports fixed relative time ranges, for example, `now-5m: now-4m`.
|
||||
|
||||
It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`.
|
||||
{{< /admonition >}}
|
||||
- TraceQL metrics queries are executed as **instant queries** over a window of time.
|
||||
- The range you select defines the aggregation window. For example use `now-5m: now-4m` for a 1-minute recording-rule interval with a 4-minute delay. The TraceQL query will run over the selected interval.
|
||||
- Always include a **delay** of a few minutes (for example, 2–5 minutes) to account for traces still in flight. This avoids missing late-arriving spans.
|
||||
- **Max data points** and **Interval** are not relevant for TraceQL instant queries.
|
||||
|
||||
- **Query**: Enter a valid TraceQL metrics query.
|
||||
```
|
||||
{ resource.service.name = "checkout" } | count_over_time()
|
||||
```
|
||||
- **Expressions**: Typically use **Reduce** > **Last** to take the most recent computed value.
|
||||
|
||||
{{< figure src="/media/docs/grafana/alerting/screenshot-recording-rule-tempo-definition-private-preview.png" alt="Defining the recording rule for Tempo." >}}
|
||||
|
||||
1. Organize the rule by selecting a **folder** and adding **labels**.
|
||||
- Labels can help group or filter rules later.
|
||||
- Folders provide UI organization and RBAC scoping.
|
||||
|
||||
1. Set evaluation behavior.
|
||||
|
||||
Recording rules are executed by evaluation groups at fixed intervals. Align this interval with the query's time range.
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
The **Evaluation interval** of the evaluation group should equal the size of the query window. For example, if your query covers `now-5m: now-4m` (1-minute window with 4-minute delay), use a **1-minute evaluation interval**. This ensures each evaluation produces a new, non-overlapping sample.
|
||||
{{< /admonition >}}
|
||||
|
||||
{{< figure src="/media/docs/grafana/alerting/screenshot-recording-rule-tempo-evaluation-group-private-preview.png" alt="Defining the recording rule evaluation group for Tempo." max-width="400px" >}}
|
||||
|
||||
## Best practices
|
||||
|
||||
- Always include a delay of at least 2 minutes in the query range to avoid losing in-flight spans.
|
||||
- Align the evaluation interval with your query window size.
|
||||
- The time series generated by a recording rule will be time-shifted based on the selected query time range. To align it back with the original timeline, you can apply the `offset` modifier in your Prometheus queries.
|
||||
@@ -207,6 +207,6 @@ The above configuration produces the following result in the Time series panel:
|
||||
|
||||
{{< figure src="/media/docs/grafana/screenshot-grafana-10-0-timeseries-time-regions.png" max-width="600px" alt="Time series visualization with time regions business hours" >}}
|
||||
|
||||
Toggle the **Advanced** switch and use [Cron syntax](https://crontab.run/) to set more granular time region controls. The following example sets a time region of 9:00 AM, Monday to Friday:
|
||||
Toggle the **Advanced** switch and use [Cron syntax](https://en.wikipedia.org/wiki/Cron) to set more granular time region controls. The following example sets a time region of 9:00 AM, Monday to Friday:
|
||||
|
||||
{{< figure src="/media/docs/grafana/dashboards/screenshot-annotations-cron-option-v11.6.png" max-width="600px" alt="Time region query with cron syntax" >}}
|
||||
|
||||
@@ -282,6 +282,8 @@ The options are:
|
||||
- **Enabled** - Show a tooltip when the cursor hovers over an element.
|
||||
- **Disabled** - Tooltips are not shown on hover.
|
||||
|
||||
The **Disable for one-click elements** setting allows hiding tooltips on elements that have one-click functionality enabled. This prevents tooltips from interfering with one-click interactions while still allowing tooltips on other elements.
|
||||
|
||||
### Layer options
|
||||
|
||||
The **Layer** options let you add elements to the canvas and control its appearance:
|
||||
|
||||
@@ -209,6 +209,7 @@ This option is only available when you're editing the panel.
|
||||
| Column width | Define a column width, in pixels, rather than allowing the width to be set automatically. By default, Grafana calculates the column width based on the table size and the minimum column width. |
|
||||
| Column alignment | Set how Grafana should align cell contents. Choose from: **Auto** (default), **Left**, **Center**, or **Right**. |
|
||||
| Column filter | Temporarily change how column data is displayed. For example, show or hide specific values. For more information, refer to [Column filtering](#column-filtering). |
|
||||
| Wrap text | Enables text wrapping for cell content. |
|
||||
| Wrap header text | Enables text wrapping for column headers. |
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@@ -288,24 +289,17 @@ The colored background cell type has the following options:
|
||||
| ------ | ----------- |
|
||||
| Background display mode | Choose between **Basic** and **Gradient**. |
|
||||
| Apply to entire row | Toggle the switch on to apply the background color that's configured for the cell to the whole row. |
|
||||
| Wrap text | Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table. To wrap the text _in a specific column only_, use a **Fields with name** [field override](ref:field-override), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch. |
|
||||
| Cell value inspect | <p>Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.</p><p>Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.</p> |
|
||||
| Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to the [Tooltip from field](#tooltip-from-field). |
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- The wrap text, cell value inspect, and tooltip from field descriptions above should be copied from docs/sources/shared/visualizations/cell-options.md -->
|
||||
<!-- The cell value inspect and tooltip from field descriptions above should be copied from docs/sources/shared/visualizations/cell-options.md -->
|
||||
|
||||
#### Data links
|
||||
|
||||
If you've configured data links, when the cell type is **Auto**, the cell text becomes clickable.
|
||||
If you change the cell type to **Data links**, the cell text reflects the titles of the configured data links. To control the application of data link text more granularly, use a **Cell option > Cell type > Data links** field override.
|
||||
|
||||
Data links cells also support text wrapping.
|
||||
Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table.
|
||||
To wrap the text _in a specific column only_, use a **Fields with name** [field override](ref:field-override), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch.
|
||||
|
||||
<!-- The wrap text description above should be copied from docs/sources/shared/visualizations/cell-options.md -->
|
||||
|
||||
#### Gauge
|
||||
|
||||
With this cell type, cells can be displayed as a graphical gauge, with several different presentation types.
|
||||
@@ -402,12 +396,6 @@ The **Pill** cell type displays each item in a comma-separated string in a color
|
||||
The colors applied to each piece of text are maintained throughout the table.
|
||||
For example, if the word "test" is first displayed in a red pill, it will always be displayed in a red pill.
|
||||
|
||||
Pill cells also support text wrapping.
|
||||
Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table.
|
||||
To wrap the text _in a specific column only_, use a **Fields with name** [field override](ref:field-override), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch.
|
||||
|
||||
<!-- The wrap text description above should be copied from docs/sources/shared/visualizations/cell-options.md -->
|
||||
|
||||
The following data formats are supported for the pill cell type:
|
||||
|
||||
- Comma-separated values (`cows,chickens,goats`)
|
||||
|
||||
@@ -18,6 +18,11 @@ title: Configure a Grafana Docker image
|
||||
weight: 1800
|
||||
---
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
Starting with Grafana release `12.4.0`, the `grafana/grafana-oss` Docker Hub repository will no longer be updated.
|
||||
Instead, we encourage you to use the `grafana/grafana` Docker Hub repository. These two repositories have the same Grafana OSS docker images.
|
||||
{{< /admonition >}}
|
||||
|
||||
# Configure a Grafana Docker image
|
||||
|
||||
This topic explains how to run Grafana on Docker in complex environments that require you to:
|
||||
@@ -27,7 +32,7 @@ This topic explains how to run Grafana on Docker in complex environments that re
|
||||
- Define secrets on the Cloud
|
||||
- Configure plugins
|
||||
|
||||
> **Note:** The examples in this topic use the Grafana Enterprise Docker image. You can use the Grafana Open Source edition by changing the Docker image to `grafana/grafana-oss`.
|
||||
> **Note:** The examples in this topic use the Grafana Enterprise Docker image. You can use the Grafana Open Source edition by changing the Docker image to `grafana/grafana`.
|
||||
|
||||
## Supported Docker image variants
|
||||
|
||||
@@ -35,7 +40,7 @@ You can install and run Grafana using the following official Docker images.
|
||||
|
||||
- **Grafana Enterprise**: `grafana/grafana-enterprise`
|
||||
|
||||
- **Grafana Open Source**: `grafana/grafana-oss`
|
||||
- **Grafana Open Source**: `grafana/grafana`
|
||||
|
||||
Each edition is available in two variants: Alpine and Ubuntu.
|
||||
|
||||
@@ -52,11 +57,11 @@ The Ubuntu-based Grafana Enterprise and OSS images are built using the [Ubuntu](
|
||||
|
||||
- **Grafana Enterprise**: `grafana/grafana-enterprise:<version>-ubuntu`
|
||||
|
||||
- **Grafana Open Source**: `grafana/grafana-oss:<version>-ubuntu`
|
||||
- **Grafana Open Source**: `grafana/grafana:<version>-ubuntu`
|
||||
|
||||
## Run a specific version of Grafana
|
||||
|
||||
You can also run a specific version of Grafana or a beta version based on the main branch of the [grafana/grafana GitHub repository](https://github.com/grafana/grafana).
|
||||
You can also run a specific version of Grafana or a beta version based on the main branch of the [`grafana/grafana` GitHub repository](https://github.com/grafana/grafana).
|
||||
|
||||
> **Note:** If you use a Linux operating system such as Debian or Ubuntu and encounter permission errors when running Docker commands, you might need to prefix the command with `sudo` or add your user to the `docker` group. The official Docker documentation provides instructions on how to [run Docker without a non-root user](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||
|
||||
@@ -74,13 +79,15 @@ The following command runs the Grafana Enterprise container and specifies versio
|
||||
docker run -d -p 3000:3000 --name grafana grafana/grafana-enterprise:9.4.7
|
||||
```
|
||||
|
||||
For recent releases of Grafana, there are also `minor` version tags in the `grafana/grafana` and `grafana/grafana-enterprise` docker repositories. For example, if you want to always have the latest `12.1` version, you can use `grafana/grafana-enterprise:12.1` or `grafana/grafana-enterprise:12.1-ubuntu`.
|
||||
|
||||
## Run the Grafana main branch
|
||||
|
||||
After every successful build of the main branch, two tags, `grafana/grafana-oss:main` and `grafana/grafana-oss:main-ubuntu`, are updated. Additionally, two new tags are created: `grafana/grafana-oss-dev:<version><build ID>-pre` and `grafana/grafana-oss-dev:<version><build ID>-pre-ubuntu`, where `version` is the next version of Grafana and `build ID `is the ID of the corresponding CI build. These tags provide access to the most recent Grafana main builds. For more information, refer to [grafana/grafana-oss-dev](https://hub.docker.com/r/grafana/grafana-oss-dev/tags).
|
||||
After every successful build of the main branch, two tags, `grafana/grafana:main` and `grafana/grafana:main-ubuntu`, are updated. Additionally, two new tags are created: `grafana/grafana-dev:<version>` and `grafana/grafana-dev:<version>-ubuntu`, where `version` is a prerelease version of Grafana. For example, if `1234` is the GitHub Run ID of the build, `12.2.0-1234`. These tags provide access to the most recent Grafana main builds. For more information, refer to [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags).
|
||||
|
||||
To ensure stability and consistency, we strongly recommend using the `grafana/grafana-oss-dev:<version><build ID>-pre` tag when running the Grafana main branch in a production environment. This tag ensures that you are using a specific version of Grafana instead of the most recent commit, which could potentially introduce bugs or issues. It also avoids polluting the tag namespace for the main Grafana images with thousands of pre-release tags.
|
||||
To ensure stability and consistency, we strongly recommend using the `grafana/grafana-dev:<version>` tag when running the Grafana main branch in a production environment. This tag ensures that you are using a specific version of Grafana instead of the most recent commit, which could potentially introduce bugs or issues. It also avoids polluting the tag namespace for the main Grafana images with thousands of pre-release tags.
|
||||
|
||||
For a list of available tags, refer to [grafana/grafana-oss](https://hub.docker.com/r/grafana/grafana-oss/tags/) and [grafana/grafana-oss-dev](https://hub.docker.com/r/grafana/grafana-oss-dev/tags/).
|
||||
For a list of available tags, refer to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/).
|
||||
|
||||
## Default paths
|
||||
|
||||
|
||||
@@ -2858,15 +2858,19 @@ Set this to `false` to disable expressions and hide them in the Grafana UI. Defa
|
||||
|
||||
#### `sql_expression_cell_limit`
|
||||
|
||||
Set the maximum number of cells that can be passed to a SQL expression. Default is `100000`.
|
||||
Set the maximum number of cells that can be passed to a SQL expression. Default is `100000`. A setting of `0` means no limit.
|
||||
|
||||
#### `sql_expression_output_cell_limit`
|
||||
|
||||
Set the maximum number of cells that can be returned from a SQL expression. Default is `100000`.
|
||||
Set the maximum number of cells that can be returned from a SQL expression. Default is `100000`. A setting of `0` means no limit.
|
||||
|
||||
### `sql_expression_query_length_limit`
|
||||
|
||||
Set the maximum length of a SQL query that can be used in a SQL expression. Default is `10000` characters. A setting of `0` means no limit.
|
||||
|
||||
#### `sql_expression_timeout`
|
||||
|
||||
The duration a SQL expression will run before being cancelled. The default is `10s`.
|
||||
The duration a SQL expression will run before being cancelled. The default is `10s`. A setting of `0s` means no limit.
|
||||
|
||||
### `[geomap]`
|
||||
|
||||
|
||||
@@ -95,8 +95,33 @@ The table below describes all SCIM configuration options. Like any other Grafana
|
||||
[auth.scim]
|
||||
user_sync_enabled = true
|
||||
group_sync_enabled = false
|
||||
reject_non_provisioned_users = false
|
||||
```
|
||||
|
||||
## Configure SCIM using Terraform
|
||||
|
||||
You can also configure SCIM provisioning in Grafana using the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/scim_config). This approach is particularly useful for infrastructure-as-code deployments and automated provisioning.
|
||||
|
||||
### Terraform SCIM configuration example
|
||||
|
||||
```hcl
|
||||
resource "grafana_scim_config" "scim_config" {
|
||||
user_sync_enabled = true
|
||||
group_sync_enabled = false
|
||||
reject_non_provisioned_users = false
|
||||
}
|
||||
```
|
||||
|
||||
### Terraform SCIM configuration options
|
||||
|
||||
The Terraform `grafana_scim_config` resource supports the same configuration options as the manual configuration:
|
||||
|
||||
| Setting | Required | Description | Default |
|
||||
| ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| `user_sync_enabled` | Yes | Enable SCIM user provisioning. When enabled, Grafana will create, update, and deactivate users based on SCIM requests from your identity provider. | `false` |
|
||||
| `group_sync_enabled` | No | Enable SCIM group provisioning. When enabled, Grafana will create, update, and delete teams based on SCIM requests from your identity provider. Cannot be enabled if Team Sync is enabled. | `false` |
|
||||
| `reject_non_provisioned_users` | No | When enabled, prevents non-SCIM provisioned users from signing in. Cloud Portal users can always sign in regardless of this setting. | `false` |
|
||||
|
||||
## Supported identity providers
|
||||
|
||||
The following identity providers are supported:
|
||||
@@ -135,5 +160,6 @@ The following table compares SCIM with other synchronization methods to help you
|
||||
## Next steps
|
||||
|
||||
- [Manage users and teams with SCIM provisioning](manage-users-teams/)
|
||||
- [Troubleshoot SCIM provisioning](troubleshooting/)
|
||||
- [Configure SCIM with Azure AD](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-scim-provisioning/configure-scim-with-azuread/)
|
||||
- [Configure SCIM with Okta](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-scim-provisioning/configure-scim-with-okta/)
|
||||
|
||||
+4
-49
@@ -272,53 +272,8 @@ Team membership maintenance:
|
||||
- Removes users from teams when removed from groups
|
||||
- Updates team memberships when groups change
|
||||
|
||||
## Troubleshooting
|
||||
## Next steps
|
||||
|
||||
### User provisioning issues
|
||||
|
||||
#### Error: "invalid namespace"
|
||||
|
||||
**Cause:** The SCIM endpoint URL is incorrectly formatted.
|
||||
|
||||
**Solution:** Verify your URL follows the correct format:
|
||||
|
||||
```bash
|
||||
https://{$GRAFANA_URL}/apis/scim.grafana.app/v0alpha1/namespaces/{$STACK_ID}/Users
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `{$GRAFANA_URL}` is your Grafana URL (subdomain format)
|
||||
- `{$STACK_ID}` is your Grafana stack ID:
|
||||
- **Grafana Cloud:** Format like `stack-123` (found in your Grafana Cloud dashboard)
|
||||
- **On-premises:** Use `default` or the name of the organization
|
||||
|
||||
### Authentication issues
|
||||
|
||||
#### Error: "HTTP 403 Forbidden"
|
||||
|
||||
**Cause:** Either incorrect token or insufficient permissions.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. **Check token:** Generate a new token from the Service Account details page
|
||||
2. **Verify permissions:** Ensure the service account has `Editor` or `Admin` role in the Grafana instance
|
||||
|
||||
#### Error: "HTTP 401 Unauthorized"
|
||||
|
||||
**Cause:** Invalid or expired authentication token.
|
||||
|
||||
**Solution:** Generate a new token from the Service Account details page in Grafana.
|
||||
|
||||
### Login issues
|
||||
|
||||
#### Error: "User sync failed"
|
||||
|
||||
**Cause:** The user's unique identifier field is not correctly configured in SAML assertions.
|
||||
|
||||
**Solution:** Add the required SAML assertion based on your identity provider:
|
||||
|
||||
| SAML Assertion | Identity Provider | Value |
|
||||
| -------------- | ----------------- | -------------------------------- |
|
||||
| `userUID` | Azure AD | `objectId` |
|
||||
| `userUID` | Okta | `user.getInternalProperty("id")` |
|
||||
- [Troubleshoot SCIM provisioning](../troubleshooting/)
|
||||
- [Configure SCIM with Azure AD](../configure-scim-with-azuread/)
|
||||
- [Configure SCIM with Okta](../configure-scim-with-okta/)
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
---
|
||||
description: Troubleshoot common SCIM provisioning issues in Grafana, including user provisioning, authentication, and login problems.
|
||||
keywords:
|
||||
- grafana
|
||||
- scim
|
||||
- troubleshooting
|
||||
- user-provisioning
|
||||
- authentication
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
menuTitle: Troubleshoot SCIM
|
||||
title: Troubleshoot SCIM provisioning
|
||||
weight: 400
|
||||
---
|
||||
|
||||
# Troubleshoot SCIM provisioning
|
||||
|
||||
This page provides solutions for common issues you might encounter when configuring and using SCIM provisioning in Grafana.
|
||||
|
||||
## User provisioning issues
|
||||
|
||||
### Error: "invalid namespace"
|
||||
|
||||
**Cause:** The SCIM endpoint URL is incorrectly formatted.
|
||||
|
||||
**Solution:** Verify your URL follows the correct format:
|
||||
|
||||
```bash
|
||||
https://{$GRAFANA_URL}/apis/scim.grafana.app/v0alpha1/namespaces/{$STACK_ID}/Users
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `{$GRAFANA_URL}` is your Grafana URL (subdomain format)
|
||||
- `{$STACK_ID}` is your Grafana stack ID:
|
||||
- **Grafana Cloud:** Format like `stack-123` (found in your Grafana Cloud dashboard)
|
||||
- **On-premises:** Use `default` or the name of the organization
|
||||
|
||||
## Authentication issues
|
||||
|
||||
### Error: "HTTP 403 Forbidden"
|
||||
|
||||
**Cause:** Either incorrect token or insufficient permissions.
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. **Check token:** Generate a new token from the Service Account details page
|
||||
2. **Verify permissions:** Ensure the service account has `Editor` or `Admin` role in the Grafana instance
|
||||
|
||||
### Error: "HTTP 401 Unauthorized"
|
||||
|
||||
**Cause:** Invalid or expired authentication token.
|
||||
|
||||
**Solution:** Generate a new token from the Service Account details page in Grafana.
|
||||
|
||||
## Login issues
|
||||
|
||||
### Error: "User sync failed"
|
||||
|
||||
**Cause:** The user's unique identifier field is not correctly configured in SAML assertions.
|
||||
|
||||
**Solution:** Add the required SAML assertion based on your identity provider:
|
||||
|
||||
| SAML Assertion | Identity Provider | Value |
|
||||
| -------------- | ----------------- | -------------------------------- |
|
||||
| `userUID` | Azure AD | `objectId` |
|
||||
| `userUID` | Okta | `user.getInternalProperty("id")` |
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Manage users and teams with SCIM provisioning](../manage-users-teams/)
|
||||
- [Configure SCIM with Azure AD](../configure-scim-with-azuread/)
|
||||
- [Configure SCIM with Okta](../configure-scim-with-okta/)
|
||||
@@ -11,6 +11,11 @@ title: Run Grafana Docker image
|
||||
weight: 400
|
||||
---
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
Starting with Grafana release `12.4.0` , the `grafana/grafana-oss` Docker Hub repository will no longer be updated.
|
||||
Instead, we encourage you to use the `grafana/grafana` Docker Hub repository. These two repositories have the same Grafana OSS docker images.
|
||||
{{< /admonition >}}
|
||||
|
||||
# Run Grafana Docker image
|
||||
|
||||
This topic guides you through installing Grafana via the official Docker images. Specifically, it covers running Grafana via the Docker command line interface (CLI) and docker-compose.
|
||||
@@ -20,7 +25,7 @@ This topic guides you through installing Grafana via the official Docker images.
|
||||
Grafana Docker images come in two editions:
|
||||
|
||||
- **Grafana Enterprise**: `grafana/grafana-enterprise`
|
||||
- **Grafana Open Source**: `grafana/grafana-oss`
|
||||
- **Grafana Open Source**: `grafana/grafana`
|
||||
|
||||
> **Note:** The recommended and default edition of Grafana is Grafana Enterprise. It is free and includes all the features of the OSS edition. Additionally, you have the option to upgrade to the [full Enterprise feature set](/products/enterprise/?utm_source=grafana-install-page), which includes support for [Enterprise plugins](/grafana/plugins/?enterprise=1&utcm_source=grafana-install-page).
|
||||
|
||||
|
||||
@@ -392,10 +392,10 @@ Instead of using the `annotate` flag, you can still use the `--record` flag. How
|
||||
|
||||
For example:
|
||||
- From
|
||||
- `yaml image: grafana/grafana-oss:10.0.1`
|
||||
- `yaml image: grafana/grafana:12.1.0`
|
||||
|
||||
- To
|
||||
- `yaml image: grafana/grafana-oss-dev:10.1.0-124419pre`
|
||||
- `yaml image: grafana/grafana-dev:12.2.0-17161637292`
|
||||
|
||||
1. Save the changes.
|
||||
|
||||
@@ -421,7 +421,7 @@ Instead of using the `annotate` flag, you can still use the `--record` flag. How
|
||||
kubectl get all --namespace=my-grafana -o wide
|
||||
```
|
||||
|
||||
You should see the newly deployed `grafana-oss-dev` image.
|
||||
You should see the newly deployed `grafana-dev` image.
|
||||
|
||||
1. To verify it, access the Grafana UI in the browser using the provided IP:Port from the command above.
|
||||
|
||||
@@ -435,7 +435,7 @@ Instead of using the `annotate` flag, you can still use the `--record` flag. How
|
||||
1. Add the `change cause` metadata to keep track of things using the commands:
|
||||
|
||||
```bash
|
||||
kubectl annotate deployment grafana --namespace=my-grafana kubernetes.io/change-cause='using grafana-oss-dev:10.1.0-124419pre for testing'
|
||||
kubectl annotate deployment grafana --namespace=my-grafana kubernetes.io/change-cause='using grafana-dev:12.2.0-17161637292 for testing'
|
||||
```
|
||||
|
||||
1. To verify, run the `kubectl rollout history` command:
|
||||
@@ -450,7 +450,7 @@ Instead of using the `annotate` flag, you can still use the `--record` flag. How
|
||||
deployment.apps/grafana
|
||||
REVISION CHANGE-CAUSE
|
||||
1 deploying the default yaml
|
||||
2 using grafana-oss-dev:10.1.0-124419pre for testing
|
||||
2 using grafana-dev:12.2.0-17161637292 for testing
|
||||
```
|
||||
|
||||
This means that `REVISION#2` is the current version.
|
||||
|
||||
@@ -5,7 +5,6 @@ title: Cell options
|
||||
<!-- prettier-ignore-start -->
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| Wrap text | Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table. To wrap the text _in a specific column only_, use a **Fields with name** [field override](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-overrides/), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch. |
|
||||
| Cell value inspect | <p>Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.</p><p>Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.</p> |
|
||||
| Tooltip from field | Toggle on the **Tooltip from field** switch to use the values from another field (or column) in a tooltip. For more information, refer to [Tooltip from field](#tooltip-from-field). |
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@@ -406,9 +406,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
|
||||
// Expand layouts section
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select tabs layout
|
||||
await page.getByLabel('Tabs').click();
|
||||
@@ -695,9 +693,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
|
||||
// Expand layouts section
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select rows layout
|
||||
await page.getByLabel('Rows').click();
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe(
|
||||
const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
|
||||
expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/);
|
||||
|
||||
await page.locator('#transparent-background').click({ force: true });
|
||||
await page.getByRole('switch', { name: 'Transparent background' }).click({ force: true });
|
||||
|
||||
const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
|
||||
expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/);
|
||||
|
||||
@@ -31,9 +31,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -64,9 +62,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -126,9 +122,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -185,9 +179,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -222,9 +214,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -279,9 +269,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -338,9 +326,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
|
||||
@@ -43,15 +43,13 @@ const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
|
||||
|
||||
const disableAllTextWrap = async (loc: Page | Locator, selectors: E2ESelectorGroups) => {
|
||||
// disable text wrapping for all of the columns, since long text with links in them can push the links off the screen.
|
||||
const wrapTextToggle = loc.locator(
|
||||
`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`
|
||||
);
|
||||
const wrapTextToggle = loc.getByLabel('Wrap text');
|
||||
const count = await wrapTextToggle.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const toggle = wrapTextToggle.nth(i);
|
||||
if ((await toggle.locator('//preceding-sibling::input').getAttribute('checked')) !== null) {
|
||||
await toggle.click();
|
||||
if ((await toggle.getAttribute('checked')) !== null) {
|
||||
await toggle.click({ force: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -75,11 +73,9 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
// text wrapping is enabled by default on this panel.
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
|
||||
|
||||
// FIXME very bad selector to get the correct "wrap text" toggle here.
|
||||
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
|
||||
await page
|
||||
.locator('[id="Override 13"]')
|
||||
.locator(`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`)
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12'))
|
||||
.getByText('Wrap text')
|
||||
.click();
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
|
||||
|
||||
@@ -95,8 +91,8 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Cell options Cell value inspect'))
|
||||
.first()
|
||||
.locator('label[for="custom.inspect"]')
|
||||
.click();
|
||||
.getByRole('switch', { name: 'Cell value inspect' })
|
||||
.click({ force: true });
|
||||
await loremIpsumCell.hover();
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
|
||||
|
||||
@@ -117,19 +113,19 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
|
||||
).toBeVisible();
|
||||
|
||||
const stateOverride = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.OptionsGroup.group('panel-options-override-11')
|
||||
);
|
||||
|
||||
// confirm that "State" column is hidden by default.
|
||||
expect(page.getByRole('row').nth(0)).not.toContainText('State');
|
||||
|
||||
// toggle the "State" column visibility and test that it appears before re-hiding it.
|
||||
// FIXME this selector is utterly godawful, but there's no way to give testIds or aria-labels or anything to
|
||||
// the panel editor builder. we should fix that to make e2e's easier to write for our team.
|
||||
const hideStateColumnSwitch = page.locator('[id="Override 12"]').locator('label').last();
|
||||
// toggle the "State" column visibility via the override we set up in the kitchen sink panel.
|
||||
const hideStateColumnSwitch = stateOverride.locator('label').last();
|
||||
await hideStateColumnSwitch.click();
|
||||
expect(page.getByRole('row').nth(0)).toContainText('State');
|
||||
|
||||
// now change the display name of the "State" column.
|
||||
// FIXME it would be good to have a better selector here too.
|
||||
const displayNameInput = page.locator('[id="Override 12"]').locator('input[value="State"]').last();
|
||||
const displayNameInput = stateOverride.locator('input[value="State"]').last();
|
||||
await displayNameInput.fill('State (renamed)');
|
||||
await displayNameInput.press('Enter');
|
||||
expect(page.getByRole('row').nth(0)).toContainText('State (renamed)');
|
||||
|
||||
+99
-100
@@ -26,6 +26,48 @@ const pluginsToTranslate = [
|
||||
'public/app/plugins/datasource/mssql',
|
||||
];
|
||||
|
||||
// [FIXME] add comment about this applying everywhere
|
||||
const baseImportConfig = {
|
||||
patterns: [
|
||||
{
|
||||
group: ['react-i18next', 'i18next'],
|
||||
importNames: ['t'],
|
||||
message: 'Please import from @grafana/i18n instead',
|
||||
},
|
||||
{
|
||||
group: ['react-i18next'],
|
||||
importNames: ['Trans'],
|
||||
message: 'Please import from @grafana/i18n instead',
|
||||
},
|
||||
{
|
||||
regex: '\\.test$',
|
||||
message:
|
||||
'Do not import test files. If you require reuse of constants/mocks across files, create a separate file with no tests',
|
||||
},
|
||||
],
|
||||
paths: [
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch', 'useSelector'],
|
||||
message: 'Please import from app/types/store instead.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{ patterns?: Array<object>, paths?: Array<object> }} config
|
||||
* @returns
|
||||
*/
|
||||
function withBaseRestrictedImportsConfig(config = {}) {
|
||||
const finalConfig = {
|
||||
patterns: [...baseImportConfig.patterns, ...(config?.patterns ?? [])],
|
||||
paths: [...baseImportConfig.paths, ...(config?.paths ?? [])],
|
||||
};
|
||||
|
||||
return finalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Array<import('eslint').Linter.Config>}
|
||||
*/
|
||||
@@ -118,35 +160,7 @@ module.exports = [
|
||||
pathGroupsExcludedImportTypes: ['builtin'],
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['react-i18next', 'i18next'],
|
||||
importNames: ['t'],
|
||||
message: 'Please import from @grafana/i18n instead',
|
||||
},
|
||||
{
|
||||
group: ['react-i18next'],
|
||||
importNames: ['Trans'],
|
||||
message: 'Please import from @grafana/i18n instead',
|
||||
},
|
||||
{
|
||||
regex: '\\.test$',
|
||||
message:
|
||||
'Do not import test files. If you require reuse of constants/mocks across files, create a separate file with no tests',
|
||||
},
|
||||
],
|
||||
paths: [
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch', 'useSelector'],
|
||||
message: 'Please import from app/types instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': ['error', baseImportConfig],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
|
||||
// Use typescript's no-redeclare for compatibility with overrides
|
||||
@@ -168,6 +182,26 @@ module.exports = [
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'grafana/no-extensions-imports',
|
||||
files: ['public/**/*.{ts,tsx,js}'],
|
||||
ignores: ['public/app/extensions/**/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
withBaseRestrictedImportsConfig({
|
||||
patterns: [
|
||||
{
|
||||
group: ['app/extensions', 'app/extensions/*'],
|
||||
message: 'Importing from app/extensions is not allowed',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'grafana/uplot-overrides',
|
||||
files: ['packages/grafana-ui/src/components/uPlot/**/*.{ts,tsx}'],
|
||||
@@ -221,76 +255,58 @@ module.exports = [
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'grafana/data-overrides',
|
||||
files: ['packages/grafana-data/**/*.{ts,tsx}'],
|
||||
ignores: ['packages/grafana-data/src/**/*.{spec,test}.{ts,tsx}'],
|
||||
// No NPM package should import from @grafana/*/internal because it does not exist
|
||||
// outside of this repo - they're not published to NPM.
|
||||
name: 'grafana/packages-overrides',
|
||||
files: ['packages/**/*.{ts,tsx}'],
|
||||
ignores: [],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@grafana/runtime', '@grafana/ui', '@grafana/data'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'grafana/ui-overrides',
|
||||
files: ['packages/grafana-ui/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@grafana/runtime', '@grafana/data/*', '@grafana/ui', '@grafana/e2e-selectors/*'],
|
||||
paths: [
|
||||
withBaseRestrictedImportsConfig({
|
||||
patterns: [
|
||||
{
|
||||
name: 'react-i18next',
|
||||
importNames: ['Trans', 't'],
|
||||
message: 'Please import from grafana-ui/src/utils/i18n instead',
|
||||
group: ['@grafana/*/internal'],
|
||||
message: "'internal' exports are not available in NPM packages because they are not published to NPM",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'grafana/schema-overrides',
|
||||
files: ['packages/grafana-schema/**/*.{ts,tsx}'],
|
||||
ignores: ['packages/grafana-schema/**/*.test.{ts,tsx}'],
|
||||
// @grafana/runtime shouldn't be imported from our 'library' NPM packages
|
||||
name: 'grafana/packages-that-cant-import-runtime',
|
||||
files: [
|
||||
'packages/grafana-ui/**/*.{ts,tsx}',
|
||||
'packages/grafana-data/**/*.{ts,tsx}',
|
||||
'packages/grafana-schema/**/*.{ts,tsx}',
|
||||
'packages/grafana-e2e-selectors/**/*.{ts,tsx}',
|
||||
],
|
||||
ignores: [],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@grafana/*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'grafana/runtime-overrides',
|
||||
files: ['packages/grafana-runtime/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@grafana/runtime', '@grafana/data/*', '@grafana/ui/*', '@grafana/e2e/*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'grafana/flamegraph-overrides',
|
||||
files: ['packages/grafana-flamegraph/**/*.{ts,tsx}'],
|
||||
ignores: ['packages/grafana-flamegraph/**/*.{test,story}.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@grafana/runtime', '@grafana/e2e', '@grafana/e2e-selectors/*'],
|
||||
},
|
||||
withBaseRestrictedImportsConfig({
|
||||
patterns: [
|
||||
{
|
||||
// Duplicated because these rules override the previous grafana/packages-overrides
|
||||
group: ['@grafana/*/internal'],
|
||||
message: "'internal' exports are not available in NPM packages because they are not published to NPM",
|
||||
},
|
||||
{
|
||||
group: ['@grafana/runtime'],
|
||||
message: "'@grafana/runtime' should not be imported from library packages",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'grafana/alerting-overrides',
|
||||
plugins: {
|
||||
@@ -427,24 +443,7 @@ module.exports = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'grafana/no-extensions-imports',
|
||||
files: ['**/*.{ts,tsx,js}'],
|
||||
ignores: ['public/app/extensions/**/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['app/extensions', 'app/extensions/*'],
|
||||
message: 'Importing from app/extensions is not allowed',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Conditionally run the betterer rules if enabled in dev's config
|
||||
// Should be last in the config so it can override any temporary disables in here
|
||||
...(enableBettererRules ? bettererConfig : []),
|
||||
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group
|
||||
connectrpc.com/connect v1.18.1 // @grafana/observability-traces-and-profiling
|
||||
cuelang.org/go v0.11.1 // @grafana/grafana-as-code
|
||||
dario.cat/mergo v1.0.2 // @grafana/grafana-app-platform-squad
|
||||
filippo.io/age v1.2.1 // @grafana/identity-access-team
|
||||
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 // @grafana/grafana-search-and-storage
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources
|
||||
@@ -285,7 +286,6 @@ require (
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
@@ -456,6 +456,7 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jaegertracing/jaeger v1.67.0 // indirect
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
@@ -517,6 +518,9 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
|
||||
@@ -582,6 +586,7 @@ require (
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.124.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect
|
||||
|
||||
@@ -634,8 +634,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSB
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
|
||||
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
|
||||
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
@@ -1793,6 +1793,8 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jaegertracing/jaeger v1.67.0 h1:t0BiJZVW9D3Z16y3uHqKzV9bKFTusooTH1Kgr77xF2Q=
|
||||
github.com/jaegertracing/jaeger v1.67.0/go.mod h1:tE/FEQfybCSdUbBgel51YaCSkc58O+Njih8oTl6j8vw=
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE=
|
||||
github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -2105,6 +2107,12 @@ github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3 h1:6jpO6
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.3/go.mod h1:dPUHjAIFzg+ci/wt6XxlNiiMkOh5Yw4SGyeRY0AFT0g=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1/go.mod h1:AFMryJmht7dZxcAwc2sx/r4gxbriElWw49ugxKp2mcA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 h1:E1e96GTHmiAfIfeYfA5ZVnOxud3+vbisGp0gE1tfd4s=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1/go.mod h1:MOhFATtYSLad9nKunjh6uGf8nQUcWje2LPlhD2uu3do=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 h1:9c6L4xlAMqhOg5y54Bc2B5t0i49yz7v2I6I8RY4Z0/o=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1/go.mod h1:6f0N58o0cOHC0ApSM/qrooVmQza1eQ7L53PDE91uO1Q=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -2546,8 +2554,13 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc=
|
||||
go.opentelemetry.io/collector/pdata v1.30.0 h1:j3jyq9um436r6WzWySzexP2nLnFdmL5uVBYAlyr9nDM=
|
||||
go.opentelemetry.io/collector/pdata v1.30.0/go.mod h1:0Bxu1ktuj4wE7PIASNSvd0SdBscQ1PLtYasymJ13/Cs=
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.124.0 h1:ZjL9wKqzP4BHj0/F1jfGxs1Va8B7xmYayipZeNVoWJE=
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.124.0/go.mod h1:1EN3Gw5LSI4fSVma/Yfv/6nqeuYgRTm1/kmG5nE5Oyo=
|
||||
go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/9dNZ0rv454goA=
|
||||
go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
|
||||
@@ -506,6 +506,7 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2 h1:YqE293IZrKtqPnpwDPH/lOqTWD/s3
|
||||
contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE=
|
||||
contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60=
|
||||
contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho=
|
||||
git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38=
|
||||
|
||||
@@ -89,6 +89,7 @@ grafana::codegen:run pkg/aggregator
|
||||
grafana::codegen:run apps/dashboard/pkg
|
||||
grafana::codegen:run apps/provisioning/pkg
|
||||
grafana::codegen:run apps/folder/pkg
|
||||
grafana::codegen:run apps/preferences/pkg
|
||||
|
||||
if [ -d "pkg/extensions/apis" ]; then
|
||||
grafana::codegen:run pkg/extensions
|
||||
|
||||
+2
-2
@@ -291,8 +291,8 @@
|
||||
"@grafana/plugin-ui": "0.10.9",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "6.30.4",
|
||||
"@grafana/scenes-react": "6.30.4",
|
||||
"@grafana/scenes": "6.32.0",
|
||||
"@grafana/scenes-react": "6.32.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -156,7 +156,7 @@ export function validatePath<OriginalPath extends string>(path: OriginalPath): O
|
||||
}
|
||||
|
||||
// Remove query params and fragments to check only the path portion
|
||||
const cleaned = originalDecoded.split(/[\?&#]/)[0];
|
||||
const cleaned = originalDecoded.split(/[\?#]/)[0];
|
||||
originalDecoded = cleaned;
|
||||
|
||||
// If the original string contains traversal attempts, block it
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user